react#FocusEvent TypeScript Examples

The following examples show how to use react#FocusEvent. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: QueryOptions.tsx    From grafana-chinese with Apache License 2.0 6 votes vote down vote up
onOverrideTime = (event: FocusEvent<HTMLInputElement>, status: InputStatus) => {
    const { value } = event.target;
    const { panel } = this.props;
    const emptyToNullValue = emptyToNull(value);
    if (status === InputStatus.Valid && panel.timeFrom !== emptyToNullValue) {
      panel.timeFrom = emptyToNullValue;
      panel.refresh();
    }
  };
Example #2
Source File: useFocus.ts    From use-platform with MIT License 6 votes vote down vote up
export function useFocus<T extends HTMLElement>(props: UseFocusProps<T>): UseFocusResult<T> {
  const propsRef = useRef(props)
  propsRef.current = props

  const onFocus = useCallback((event: FocusEvent<T>) => {
    const { onFocus, onFocusChange } = propsRef.current

    if (event.target === event.currentTarget) {
      onFocus?.(event)
      onFocusChange?.(true)
    }
  }, [])

  const onBlur = useCallback((event: FocusEvent<T>) => {
    const { onBlur, onFocusChange } = propsRef.current

    if (event.target === event.currentTarget) {
      onBlur?.(event)
      onFocusChange?.(false)
    }
  }, [])

  const focusProps: HTMLAttributes<T> = {}

  if (!props.disabled) {
    if (props.onFocus || props.onFocusChange) {
      focusProps.onFocus = onFocus
    }

    if (props.onBlur || props.onFocusChange) {
      focusProps.onBlur = onBlur
    }
  }

  return {
    focusProps,
  }
}
Example #3
Source File: SimVarControlElement.tsx    From ace with GNU Affero General Public License v3.0 6 votes vote down vote up
EditableSimVarControlValue: FC<EditableSimVarControlValueProps> = ({ value, unit, onInput }) => {
    const editSpanRef = useRef<HTMLSpanElement>(null);

    useEffect(() => {
        if (editSpanRef.current) {
            editSpanRef.current.textContent = String(value);
        }
    }, [value]);

    const handleBlur = useCallback((e: FocusEvent) => {
        onInput((e.target as HTMLElement).textContent);
    }, [onInput]);

    return (
        <code className="mr-auto font-semibold">
            <span
                ref={editSpanRef}
                contentEditable
                suppressContentEditableWarning
                className="inline-block outline-none bg-navy-medium text-green-500 rounded-md px-2 py-1"
                onBlur={handleBlur}
                style={{
                    minWidth: '4rem',
                    maxWidth: '8rem',
                }}
            />
            {' '}
            <span className="text-green-400">
                {unit}
            </span>
        </code>
    );
}
Example #4
Source File: QueryOptions.tsx    From grafana-chinese with Apache License 2.0 6 votes vote down vote up
onTimeShift = (event: FocusEvent<HTMLInputElement>, status: InputStatus) => {
    const { value } = event.target;
    const { panel } = this.props;
    const emptyToNullValue = emptyToNull(value);
    if (status === InputStatus.Valid && panel.timeShift !== emptyToNullValue) {
      panel.timeShift = emptyToNullValue;
      panel.refresh();
    }
  };
Example #5
Source File: layouts.ts    From geist-ui with MIT License 6 votes vote down vote up
useRect = (initialState?: ReactiveDomReact | (() => ReactiveDomReact)) => {
  const [rect, setRect] = useState<ReactiveDomReact>(initialState || defaultRect)

  const updateRect = (
    eventOrRef:
      | MouseEvent<HTMLElement>
      | FocusEvent<HTMLElement>
      | MutableRefObject<HTMLElement | null>,
    getContainer?: () => HTMLElement | null,
  ) => {
    if (isRefTarget(eventOrRef)) return setRect(getRefRect(eventOrRef, getContainer))
    setRect(getEventRect(eventOrRef, getContainer))
  }

  return {
    rect,
    setRect: updateRect,
  }
}
Example #6
Source File: TextBoxVariablePicker.tsx    From grafana-chinese with Apache License 2.0 5 votes vote down vote up
onQueryBlur = (event: FocusEvent<HTMLInputElement>) => {
    if (this.props.variable.current.value !== this.props.variable.query) {
      variableAdapters.get(this.props.variable.type).updateOptions(this.props.variable);
    }
  };
Example #7
Source File: SearchInput.tsx    From react-search-autocomplete with MIT License 5 votes vote down vote up
export default function SearchInput({
  searchString,
  setSearchString,
  setHighlightedItem,
  autoFocus,
  onBlur,
  onFocus,
  onClear,
  placeholder,
  showIcon = true,
  showClear = true
}: SearchInputProps) {
  const ref = useRef<HTMLInputElement>(null)

  let manualFocus = true

  const setFocus = () => {
    manualFocus = false
    ref?.current && ref.current.focus()
    manualFocus = true
  }

  const handleOnFocus = (event: FocusEvent<HTMLInputElement, Element>) => {
    manualFocus && onFocus(event)
  }

  return (
    <StyledSearchInput>
      <SearchIcon showIcon={showIcon} />
      <input
        ref={ref}
        spellCheck={false}
        value={searchString}
        onChange={setSearchString}
        onBlur={onBlur}
        onFocus={handleOnFocus}
        placeholder={placeholder}
        autoFocus={autoFocus}
        onKeyDown={(event) => setHighlightedItem({ event })}
      />
      <ClearIcon
        showClear={showClear}
        setSearchString={setSearchString}
        searchString={searchString}
        onClear={onClear}
        setFocus={setFocus}
      />
    </StyledSearchInput>
  )
}
Example #8
Source File: ConstantVariableEditor.tsx    From grafana-chinese with Apache License 2.0 5 votes vote down vote up
onBlur = (event: FocusEvent<HTMLInputElement>) => {
    this.props.onPropChange({
      propName: 'query',
      propValue: event.target.value,
      updateOptions: true,
    });
  };
Example #9
Source File: NumberInput.tsx    From ble with Apache License 2.0 5 votes vote down vote up
NumberInput: FunctionComponent<Props> = ({ value, onChange, onBlur, min, max, step, ...props }) => {
	function reducer(state: State, action: Action): State {
		switch (action.type) {
			case 'set': {
				const safeValue = !isValid(action.valueAsNumber, { min, max, step }) ? state.latestValidValue : action.valueAsNumber;
				return {
					...state,
					innerValue: action.value,
					latestValidValue: safeValue,

				};
			}
			case 'resetLatestSafe':
				return {
					...state,
					innerValue: state.latestValidValue.toString(),
				};
			default:
				throw new Error('Invalid action type');
		}
	}
	const [{ innerValue, latestValidValue }, dispatch] = useReducer(reducer, {
		innerValue: value.toString(),
		latestValidValue: value,
	});

	useEffect(() => {
		dispatch({
			type: 'set',
			value: value.toString(),
			valueAsNumber: value,
		});
	}, [value]);

	// state updates are asynchronous
	// so we do our onChange here
	useEffect(() => {
		if (onChange !== undefined && latestValidValue !== value) {
			onChange(latestValidValue);
		}
	}, [latestValidValue]);

	function onInnerChange(ev: ChangeEvent<HTMLInputElement>): void {
		dispatch({
			type: 'set',
			value: ev.target.value,
			valueAsNumber: ev.target.valueAsNumber,
		});
	}

	// put the latest correct value when losing focus
	function onInnerBlur(ev: FocusEvent<HTMLInputElement>): void {
		dispatch({
			type: 'resetLatestSafe',
		});

		if (onBlur !== undefined) {
			onBlur(ev);
		}
	}

	return (
		<input {...props} type="number" value={innerValue} onChange={onInnerChange} onBlur={onInnerBlur}/>
	);
}
Example #10
Source File: CustomVariableEditor.tsx    From grafana-chinese with Apache License 2.0 5 votes vote down vote up
onBlur = (event: FocusEvent<HTMLInputElement>) => {
    this.props.onPropChange({
      propName: 'query',
      propValue: event.target.value,
      updateOptions: true,
    });
  };
Example #11
Source File: InfiniteRange.tsx    From ble with Apache License 2.0 5 votes vote down vote up
NumberInput: FunctionComponent<Props> = ({ value, onChange, onBlur, onPointerUp, min = 0, step, ...props }) => {
	const maxClamp = Math.max(min, props.maxClamp || 100);
	const [max, setMax] = useState(maxClamp);

	function clampedSetMax(max_: number) {
		setMax(Math.max(max_, maxClamp));
	}

	function onInnerChange(ev: ChangeEvent<HTMLInputElement>) {
		if (onChange) {
			onChange(ev.target.valueAsNumber);
		}
	}

	function onInnerBlur(ev: FocusEvent<HTMLInputElement>) {
		clampedSetMax(ev.target.valueAsNumber * 2);

		if (onBlur) {
			onBlur(ev);
		}
	}

	function onInnerPointerUp(ev: PointerEvent<HTMLInputElement>) {
		// we keep the value for our setTimeout
		const val = (ev.target as HTMLInputElement).valueAsNumber;

		// we wait for the next tick to let onChange happen first
		window.setTimeout(() => {
			clampedSetMax(val * 2);
		}, 0);

		if (onPointerUp) {
			onPointerUp(ev);
		}
	}

	return (
		<input
			{...props}
			type="range"
			value={value}
			min={min}
			max={max}
			onChange={onInnerChange}
			onBlur={onInnerBlur}
			onPointerUpCapture={onInnerPointerUp}
		/>
	);
}
Example #12
Source File: DestinationParam.tsx    From ble with Apache License 2.0 5 votes vote down vote up
DestinationParam: FunctionComponent<Props> = ({ params }) => {
	const [destination, setDestination] = useState(params.destination);

	useEffect(() => {
		setDestination(params.destination);
	}, [params.destination]);

	const onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
		setDestination(ev.target.value);
		// we must cancel the potential previous invalid state
		// https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity
		ev.target.setCustomValidity('');

		const valid = ev.target.checkValidity();

		if (valid) {
			const value = ev.target.value === '' ? undefined : ev.target.value;
			params.setDestination(value);
		}
	};

	const onBlur = (ev: FocusEvent<HTMLInputElement>): void => {
		const valid = ev.target.checkValidity();
		if (!valid) {
			ev.target.setCustomValidity('Must be a level URL or a level ID');
			ev.target.reportValidity();
		}
	};

	return (
		<label title="The level where this door leads. Leave empty to go to the menu.">
			<FontAwesomeIcon icon={faLink}/>
			&#32;
			destination level:
			&#32;
			<input
				type="text"
				value={destination || ''}
				onChange={onChange} placeholder="https://bombhopper.io/?level=b3cd72ad-e47c-4aac-a720-3ea871d0330c"
				pattern={doorDestinationRegex.source}
				onBlur={onBlur}
			/>
		</label>
	);
}
Example #13
Source File: layouts.ts    From geist-ui with MIT License 5 votes vote down vote up
isRefTarget = (
  eventOrRef:
    | MouseEvent<HTMLElement>
    | FocusEvent<HTMLElement>
    | MutableRefObject<HTMLElement | null>,
): eventOrRef is MutableRefObject<HTMLElement | null> => {
  return typeof (eventOrRef as any)?.target === 'undefined'
}
Example #14
Source File: layouts.ts    From geist-ui with MIT License 5 votes vote down vote up
getEventRect = (
  event?: MouseEvent<HTMLElement> | FocusEvent<HTMLElement>,
  getContainer?: () => HTMLElement | null,
) => {
  const rect = (event?.target as HTMLElement)?.getBoundingClientRect()
  if (!rect) return defaultRect
  return getRectFromDOMWithContainer(rect, getContainer)
}
Example #15
Source File: useFocusWithin.ts    From use-platform with MIT License 5 votes vote down vote up
export function useFocusWithin<T extends HTMLElement>(
  props: UseFocusWithinProps<T>,
): UseFocusWithinResult<T> {
  const propsRef = useRef(props)
  const stateRef = useRef({ within: false })
  propsRef.current = props

  const onFocus = useCallback((event: FocusEvent<T>) => {
    const { onFocusWithin, onFocusWithinChange } = propsRef.current
    const { current: state } = stateRef

    if (!state.within) {
      state.within = true

      onFocusWithin?.(event)
      onFocusWithinChange?.(true)
    }
  }, [])

  const onBlur = useCallback((event: FocusEvent<T>) => {
    const { onBlurWithin, onFocusWithinChange } = propsRef.current
    const { current: state } = stateRef

    if (state.within && !event.currentTarget.contains(event.relatedTarget as HTMLElement | null)) {
      state.within = false

      onBlurWithin?.(event)
      onFocusWithinChange?.(false)
    }
  }, [])

  const focusWithinProps: HTMLAttributes<T> = {}

  if (!props.disabled && (props.onFocusWithin || props.onBlurWithin || props.onFocusWithinChange)) {
    focusWithinProps.onFocus = onFocus
    focusWithinProps.onBlur = onBlur
  }

  return {
    focusWithinProps,
  }
}
Example #16
Source File: useCalendar.ts    From use-platform with MIT License 4 votes vote down vote up
export function useCalendar(props: UseCalendarProps, state: CalendarState): UseCalendarResult {
  const { readOnly, disabled } = props
  const { mode, focusedDate, focusCalendar, focusDate, moveDate } = state
  const { isRTL } = useLocale()

  const onFocus = () => {
    focusCalendar(true)
  }

  const onBlur = (event: FocusEvent) => {
    const { relatedTarget, currentTarget } = event

    if (!currentTarget.contains(relatedTarget as HTMLElement)) {
      focusCalendar(false)
    }
  }

  const onKeyDown = (event: KeyboardEvent<HTMLElement>) => {
    let action: CalendarNavigationAction | null = null

    switch (event.key) {
      case 'ArrowLeft':
        action = isRTL ? CalendarNavigationAction.NextCell : CalendarNavigationAction.PrevCell
        break

      case 'ArrowRight':
        action = isRTL ? CalendarNavigationAction.PrevCell : CalendarNavigationAction.NextCell
        break

      case 'ArrowUp':
        action = CalendarNavigationAction.UpperCell
        break

      case 'ArrowDown':
        action = CalendarNavigationAction.LowerCell
        break

      case 'Home':
        action = event.shiftKey
          ? CalendarNavigationAction.FirstCell
          : CalendarNavigationAction.StartCell
        break

      case 'End':
        action = event.shiftKey
          ? CalendarNavigationAction.LastCell
          : CalendarNavigationAction.EndCell
        break

      case 'PageUp':
        action = event.shiftKey
          ? CalendarNavigationAction.PrevExtraView
          : CalendarNavigationAction.PrevView
        break

      case 'PageDown':
        action = event.shiftKey
          ? CalendarNavigationAction.NextExtraView
          : CalendarNavigationAction.NextView
        break
    }

    if (action !== null) {
      event.preventDefault()
      focusDate(moveDate(focusedDate, action))
    }
  }

  const gridProps: HTMLAttributes<HTMLElement> = {
    role: 'grid',
    'aria-readonly': readOnly,
    'aria-disabled': disabled,
    'aria-multiselectable': mode === 'multiple' || mode === 'range',
    onFocus,
    onBlur,
    onKeyDown,
  }

  return {
    gridProps,
  }
}
Example #17
Source File: modal-entry.tsx    From keycaplendar with MIT License 4 votes vote down vote up
ModalEntry = ({
  keyset: propsKeyset,
  loading,
  onClose,
  onSubmit,
  open,
  user,
}: ModalEntryProps) => {
  const device = useAppSelector(selectDevice);

  const allDesigners = useAppSelector(selectAllDesigners);
  const allProfiles = useAppSelector(selectAllProfiles);
  const allVendors = useAppSelector(selectAllVendors);
  const allVendorRegions = useAppSelector(selectAllVendorRegions);

  const [keyset, updateKeyset] = useImmer<KeysetState>(
    partialSet({ alias: nanoid(10) })
  );

  const keyedKeysetUpdate =
    <K extends keyof KeysetState>(key: K, payload: KeysetState[K]) =>
    (draft: KeysetState) => {
      draft[key] = payload;
    };

  const [salesImageLoaded, setSalesImageLoaded] = useState(false);

  const [focused, setFocused] = useState("");

  useEffect(() => {
    if (!user.isEditor && user.isDesigner) {
      updateKeyset(keyedKeysetUpdate("designer", [user.nickname]));
    }
    if (!open) {
      updateKeyset(partialSet({ alias: nanoid(10) }));
      setSalesImageLoaded(false);
      setFocused("");
    }
  }, [open]);

  useEffect(() => {
    if (propsKeyset) {
      updateKeyset(
        produce(propsKeyset, (draft) => {
          draft.alias ??= nanoid(10);
          draft.gbMonth ??= false;
          draft.notes ??= "";
          draft.sales ??= {
            img: "",
            thirdParty: false,
          };
          draft.sales.img ??= "";
          draft.sales.thirdParty ??= false;
          draft.vendors ??= [];
          draft.vendors.forEach((vendor) => {
            vendor.id ??= nanoid();
          });
          if (draft.gbMonth && draft.gbLaunch.length === 10) {
            draft.gbLaunch = draft.gbLaunch.slice(0, 7);
          }
        })
      );
    }
  }, [propsKeyset, open]);

  const setImage = (image: Blob | File) =>
    updateKeyset(keyedKeysetUpdate("image", image));

  const handleFocus = (e: FocusEvent<HTMLInputElement>) =>
    setFocused(e.target.name);

  const handleBlur = () => setFocused("");

  const toggleDate = () =>
    updateKeyset((keyset) => {
      keyset.gbMonth = !keyset.gbMonth;
    });

  const selectValue = (prop: string, value: string) => {
    if (hasKey(keyset, prop)) {
      updateKeyset(keyedKeysetUpdate(prop, value));
    }
    setFocused("");
  };

  const selectValueAppend = (prop: string, value: string) =>
    updateKeyset((draft) => {
      if (hasKey(draft, prop)) {
        const { [prop]: original } = draft;
        if (original) {
          if (is<string[]>(original)) {
            original[original.length - 1] = value;
            setFocused("");
          } else if (is<string>(original)) {
            const array = original.split(", ");
            array[array.length - 1] = value;
            draft[prop as KeysMatching<KeysetState, string>] = array.join(", ");
            setFocused("");
          }
        }
      }
    });

  const selectVendor = (prop: string, value: string) => {
    const property = prop.replace(/\d/g, "");
    const index = parseInt(prop.replace(/\D/g, ""));
    updateKeyset((draft) => {
      const {
        vendors: { [index]: vendor },
      } = draft;
      if (hasKey(vendor, property)) {
        vendor[property] = value;
        setFocused("");
      }
    });
  };

  const selectVendorAppend = (prop: string, value: string) => {
    const property = prop.replace(/\d/g, "");
    const index = parseInt(prop.replace(/\D/g, ""));
    updateKeyset((draft) => {
      const {
        vendors: { [index]: vendor },
      } = draft;
      if (hasKey(vendor, property)) {
        const { [property]: original } = vendor;
        if (typeof original !== "undefined") {
          const array = original.split(", ");
          array[array.length - 1] = value;
          vendor[property] = array.join(", ");
          setFocused("");
        }
      }
    });
  };

  const handleChange = ({
    target: { checked, name, value },
  }: ChangeEvent<HTMLInputElement>) => {
    if (name === "designer") {
      updateKeyset(keyedKeysetUpdate(name, value.split(", ")));
    } else if (name === "shipped") {
      updateKeyset(keyedKeysetUpdate(name, checked));
    } else if (hasKey(keyset, name)) {
      updateKeyset(keyedKeysetUpdate(name, value));
    }
  };

  const handleSalesImage = ({
    target: { checked, name, value },
  }: ChangeEvent<HTMLInputElement>) => {
    if (hasKey(keyset.sales, name)) {
      if (name === "thirdParty") {
        updateKeyset((keyset) => {
          keyset.sales[name] = checked;
        });
      } else {
        updateKeyset((keyset) => {
          keyset.sales[name] = value;
        });
      }
    }
  };

  const handleNamedChange =
    <Key extends keyof KeysetState>(name: Key) =>
    (value: KeysetState[Key]) =>
      updateKeyset(keyedKeysetUpdate(name, value));

  const handleChangeVendor = ({
    target: { name, value },
  }: ChangeEvent<HTMLInputElement>) => {
    const property = name.replace(/\d/g, "");
    const index = parseInt(name.replace(/\D/g, ""));
    updateKeyset((draft) => {
      const {
        vendors: { [index]: vendor },
      } = draft;
      if (hasKey(vendor, property)) {
        vendor[property] = value;
      }
    });
  };

  const handleNamedChangeVendor =
    (name: keyof VendorType, index: number) => (value: string) =>
      updateKeyset((draft) => {
        const {
          vendors: { [index]: vendor },
        } = draft;
        if (hasKey(vendor, name)) {
          vendor[name] = value;
        }
      });

  const handleChangeVendorEndDate = (e: ChangeEvent<HTMLInputElement>) => {
    const index = parseInt(e.target.name.replace(/\D/g, ""));
    updateKeyset((draft) => {
      const {
        vendors: { [index]: vendor },
      } = draft;
      if (e.target.checked) {
        vendor.endDate = "";
      } else {
        delete vendor.endDate;
      }
    });
  };

  const addVendor = () => {
    const emptyVendor = {
      id: nanoid(),
      name: "",
      region: "",
      storeLink: "",
    };
    updateKeyset((draft) => {
      draft.vendors.push(emptyVendor);
    });
  };

  const removeVendor = (index: number) =>
    updateKeyset((draft) => {
      draft.vendors.splice(index, 1);
    });

  const handleDragVendor = (result: DropResult) => {
    if (!result.destination) return;
    updateKeyset((draft) => {
      arrayMove(
        draft.vendors,
        result.source.index,
        result.destination?.index || 0
      );
    });
  };

  const result = useMemo(
    () =>
      gbMonthCheck(
        SetSchema.extend({
          id: z.string().min(propsKeyset ? 1 : 0),
          image: z.union([z.string().url(), z.instanceof(Blob)]),
        })
      ).safeParse(keyset),
    [keyset]
  );

  const useDrawer = device !== "mobile";
  const dateCard = keyset.gbMonth ? (
    <Card className="date-container" outlined>
      <Typography className="date-title" tag="h3" use="caption">
        Month
      </Typography>
      <div className="date-form">
        <DatePicker
          allowQuarter
          autoComplete="off"
          icon={iconObject(<CalendarToday />)}
          label="GB month"
          month
          name="gbLaunch"
          onChange={handleNamedChange("gbLaunch")}
          outlined
          showNowButton
          value={keyset.gbLaunch}
        />
      </div>
      <CardActions>
        <CardActionButtons>
          <CardActionButton label="Date" onClick={toggleDate} type="button" />
        </CardActionButtons>
      </CardActions>
    </Card>
  ) : (
    <Card className="date-container" outlined>
      <Typography className="date-title" tag="h3" use="caption">
        Date
      </Typography>
      <div className="date-form">
        <DatePicker
          allowQuarter
          autoComplete="off"
          icon={iconObject(<CalendarToday />)}
          label="GB launch"
          name="gbLaunch"
          onChange={handleNamedChange("gbLaunch")}
          outlined
          showNowButton
          value={keyset.gbLaunch}
        />
        <DatePicker
          autoComplete="off"
          fallbackValue={keyset.gbLaunch}
          icon={iconObject(<CalendarToday />)}
          label="GB end"
          name="gbEnd"
          onChange={handleNamedChange("gbEnd")}
          outlined
          showNowButton
          value={keyset.gbEnd}
        />
      </div>
      <CardActions>
        <CardActionButtons>
          <CardActionButton label="Month" onClick={toggleDate} type="button" />
        </CardActionButtons>
      </CardActions>
    </Card>
  );
  return (
    <BoolWrapper
      condition={useDrawer}
      falseWrapper={(children) => (
        <FullScreenDialog className="entry-modal" {...{ onClose, open }}>
          {children}
        </FullScreenDialog>
      )}
      trueWrapper={(children) => (
        <Drawer
          className="drawer-right entry-modal"
          modal
          {...{ onClose, open }}
        >
          {children}
        </Drawer>
      )}
    >
      <BoolWrapper
        condition={useDrawer}
        falseWrapper={(children) => (
          <FullScreenDialogAppBar>
            <TopAppBarRow>{children}</TopAppBarRow>
            <LinearProgress
              closed={!loading}
              progress={typeof loading === "number" ? loading : undefined}
            />
          </FullScreenDialogAppBar>
        )}
        trueWrapper={(children) => (
          <DrawerHeader>
            {children}
            <LinearProgress
              closed={!loading}
              progress={typeof loading === "number" ? loading : undefined}
            />
          </DrawerHeader>
        )}
      >
        <BoolWrapper
          condition={useDrawer}
          falseWrapper={(children) => (
            <TopAppBarSection alignStart>
              <TopAppBarNavigationIcon icon="close" onClick={onClose} />
              <TopAppBarTitle>{children}</TopAppBarTitle>
            </TopAppBarSection>
          )}
          trueWrapper={(children) => <DrawerTitle>{children}</DrawerTitle>}
        >
          {propsKeyset ? "Edit" : "Create"} Entry
        </BoolWrapper>

        <ConditionalWrapper
          condition={!useDrawer}
          wrapper={(children) => (
            <TopAppBarSection alignEnd>{children}</TopAppBarSection>
          )}
        >
          <Button
            disabled={!result?.success || !!loading}
            label="Save"
            onClick={() => onSubmit(keyset)}
            outlined={useDrawer}
            type="button"
          />
        </ConditionalWrapper>
      </BoolWrapper>
      <BoolWrapper
        condition={useDrawer}
        falseWrapper={(children) => (
          <FullScreenDialogContent>{children}</FullScreenDialogContent>
        )}
        trueWrapper={(children) => <DrawerContent>{children}</DrawerContent>}
      >
        <div className="banner">
          <div className="banner-text">Make sure to read the entry guide.</div>
          <div className="banner-button">
            <a
              href="/guides?guideId=JLB4xxfx52NJmmnbvbzO"
              rel="noopener noreferrer"
              target="_blank"
            >
              <Button label="guide" />
            </a>
          </div>
        </div>
        <form className="form">
          <div className="form-double">
            <div className="select-container">
              <MenuSurfaceAnchor>
                <TextField
                  autoComplete="off"
                  label="Profile"
                  name="profile"
                  onBlur={handleBlur}
                  onChange={handleChange}
                  onFocus={handleFocus}
                  outlined
                  required
                  value={keyset.profile}
                />
                <Autocomplete
                  array={allProfiles}
                  minChars={1}
                  open={focused === "profile"}
                  prop="profile"
                  query={keyset.profile}
                  select={selectValue}
                />
              </MenuSurfaceAnchor>
            </div>
            <div className="field-container">
              <TextField
                autoComplete="off"
                className="field"
                label="Colorway"
                name="colorway"
                onBlur={handleBlur}
                onChange={handleChange}
                onFocus={handleFocus}
                outlined
                required
                value={keyset.colorway}
              />
            </div>
          </div>
          <MenuSurfaceAnchor>
            <TextField
              autoComplete="off"
              disabled={user.isEditor === false && user.isDesigner}
              helpText={{
                children: (
                  <>
                    Separate multiple designers with{" "}
                    <code className="multiline">, </code>.
                  </>
                ),
                persistent: true,
              }}
              label="Designer"
              name="designer"
              onBlur={handleBlur}
              onChange={handleChange}
              onFocus={handleFocus}
              outlined
              required
              value={keyset.designer.join(", ")}
            />
            <Autocomplete
              array={allDesigners}
              listSplit
              minChars={2}
              open={focused === "designer"}
              prop="designer"
              query={keyset.designer.join(", ")}
              select={selectValueAppend}
            />
          </MenuSurfaceAnchor>
          <DatePicker
            autoComplete="off"
            icon={iconObject(<CalendarToday />)}
            label="IC date"
            name="icDate"
            onChange={handleNamedChange("icDate")}
            outlined
            pickerProps={{ disableFuture: true }}
            required
            showNowButton
            value={keyset.icDate}
          />
          <TextField
            autoComplete="off"
            helpText={{
              children: "Must be valid link",
              persistent: false,
              validationMsg: true,
            }}
            icon="link"
            label="Details"
            name="details"
            onBlur={handleBlur}
            onChange={handleChange}
            onFocus={handleFocus}
            outlined
            pattern={validLink}
            required
            value={keyset.details}
          />
          <TextField
            autoComplete="off"
            label="Notes"
            name="notes"
            onBlur={handleBlur}
            onChange={handleChange}
            onFocus={handleFocus}
            outlined
            rows={2}
            textarea
            value={keyset.notes}
          />
          <ImageUpload
            desktop={device === "desktop"}
            image={keyset.image}
            setImage={setImage}
          />
          {dateCard}
          <Checkbox
            checked={keyset.shipped}
            id="create-shipped"
            label="Shipped"
            name="shipped"
            onChange={handleChange}
          />
          <Typography className="subheader" tag="h3" use="caption">
            Vendors
          </Typography>
          <DragDropContext onDragEnd={handleDragVendor}>
            <Droppable droppableId="vendors-create">
              {(provided) => (
                <div
                  ref={provided.innerRef}
                  className="vendors-container"
                  {...provided.droppableProps}
                >
                  {keyset.vendors.map((vendor, index) => {
                    const endDateField =
                      typeof vendor.endDate === "string" ? (
                        <DatePicker
                          autoComplete="off"
                          icon={iconObject(<CalendarToday />)}
                          label="End date"
                          name={`endDate${index}`}
                          onChange={handleNamedChangeVendor("endDate", index)}
                          outlined
                          required
                          showNowButton
                          value={vendor.endDate}
                        />
                      ) : null;
                    return (
                      <Draggable
                        key={vendor.id}
                        draggableId={vendor.id ?? index.toString()}
                        index={index}
                      >
                        {(provided, snapshot) => (
                          <Card
                            ref={provided.innerRef}
                            className={classNames("vendor-container", {
                              dragged: snapshot.isDragging,
                            })}
                            outlined
                            {...provided.draggableProps}
                            style={getVendorStyle(provided)}
                          >
                            <div className="title-container">
                              <Typography
                                className="vendor-title"
                                use="caption"
                              >
                                {`Vendor ${index + 1}`}
                              </Typography>
                              {withTooltip(
                                <IconButton
                                  icon={iconObject(<Delete />)}
                                  onClick={() => {
                                    removeVendor(index);
                                  }}
                                  type="button"
                                />,
                                "Delete"
                              )}
                              {withTooltip(
                                <Icon
                                  className="drag-handle"
                                  icon="drag_handle"
                                  {...provided.dragHandleProps}
                                />,
                                "Drag"
                              )}
                            </div>
                            <div className="vendor-form">
                              <MenuSurfaceAnchor>
                                <TextField
                                  autoComplete="off"
                                  icon={iconObject(<Store />)}
                                  label="Name"
                                  name={`name${index}`}
                                  onBlur={handleBlur}
                                  onChange={handleChangeVendor}
                                  onFocus={handleFocus}
                                  outlined
                                  required
                                  value={vendor.name}
                                />
                                <Autocomplete
                                  array={allVendors}
                                  minChars={1}
                                  open={focused === `name${index}`}
                                  prop={`name${index}`}
                                  query={vendor.name}
                                  select={selectVendor}
                                />
                              </MenuSurfaceAnchor>
                              <MenuSurfaceAnchor>
                                <TextField
                                  autoComplete="off"
                                  icon={iconObject(<Public />)}
                                  label="Region"
                                  name={`region${index}`}
                                  onBlur={handleBlur}
                                  onChange={handleChangeVendor}
                                  onFocus={handleFocus}
                                  outlined
                                  required
                                  value={vendor.region}
                                />
                                <Autocomplete
                                  array={allVendorRegions}
                                  listSplit
                                  minChars={1}
                                  open={focused === `region${index}`}
                                  prop={`region${index}`}
                                  query={vendor.region}
                                  select={selectVendorAppend}
                                />
                              </MenuSurfaceAnchor>
                              <TextField
                                autoComplete="off"
                                helpText={{
                                  children: "Must be valid link",
                                  persistent: false,
                                  validationMsg: true,
                                }}
                                icon="link"
                                label="Store link"
                                name={`storeLink${index}`}
                                onBlur={handleBlur}
                                onChange={handleChangeVendor}
                                onFocus={handleFocus}
                                outlined
                                pattern={validLink}
                                value={vendor.storeLink}
                              />
                              <Checkbox
                                checked={
                                  !!vendor.endDate || vendor.endDate === ""
                                }
                                className="end-date-field"
                                id={`editEndDate${index}`}
                                label="Different end date"
                                name={`endDate${index}`}
                                onChange={handleChangeVendorEndDate}
                              />
                              {endDateField}
                            </div>
                          </Card>
                        )}
                      </Draggable>
                    );
                  })}
                  {provided.placeholder}
                </div>
              )}
            </Droppable>
          </DragDropContext>
          <div className="add-button">
            <Button
              label="Add vendor"
              onClick={addVendor}
              outlined
              type="button"
            />
          </div>
          <Card className="sales-container" outlined>
            <Typography className="sales-title" tag="h3" use="caption">
              Sales
            </Typography>
            <div
              className={classNames("sales-image", {
                loaded: salesImageLoaded,
              })}
            >
              <div className="sales-image-icon">
                <Icon icon={iconObject(<AddPhotoAlternate />)} />
              </div>
              <img
                alt=""
                onError={() => {
                  setSalesImageLoaded(false);
                }}
                onLoad={() => {
                  setSalesImageLoaded(true);
                }}
                src={keyset.sales.img}
              />
            </div>
            <div className="sales-field">
              <TextField
                autoComplete="off"
                helpText={{
                  children: "Must be direct link to image",
                  persistent: true,
                  validationMsg: true,
                }}
                icon="link"
                label="URL"
                name="img"
                onBlur={handleBlur}
                onChange={handleSalesImage}
                onFocus={handleFocus}
                outlined
                pattern={validLink}
                value={keyset.sales.img}
              />
              <Checkbox
                checked={keyset.sales.thirdParty}
                className="sales-checkbox"
                label="Third party graph"
                name="thirdParty"
                onChange={handleSalesImage}
              />
            </div>
          </Card>
        </form>
      </BoolWrapper>
    </BoolWrapper>
  );
}
Example #18
Source File: useAutocomplete.ts    From kodiak-ui with MIT License 4 votes vote down vote up
export function useAutocomplete({
  isOpen: isOpenProp,
  isClearable = true,
  isDisabled = false,
  isMulti = false,
  value: valueProp,
  inputValue: inputValueProp,
  componentName = 'useAutocomplete',
  defaultValue = null,
  openOnFocus = true,
  pageSize = 5,
  blurOnSelect = false,
  clearOnBlur = false,
  clearOnEscape = false,
  closeOnSelect = true,
  options,

  onCloseChange,
  onOpenChange,
  onValueChange,
  onInputValueChange,
  onHighlightedIndexChange,

  getOptionSelected = (option, value) => option === value,
  getOptionDisabled,
}: UseAutocompleteProps) {
  const inputRef = useRef<HTMLInputElement>(null)
  const listboxRef = useRef<HTMLElement>(null)
  const highlightedIndexRef = useRef<number>(-1)

  const uid = useId()
  const id = `kodiak-autocomplete-${uid}`

  const [isOpen, setIsOpen] = useControlled<boolean>({
    controlled: isOpenProp,
    default: false,
    name: componentName,
    state: 'open',
  })

  const [value, setValue] = useControlled<string | string[]>({
    controlled: valueProp,
    default: defaultValue,
    name: componentName,
    state: 'value',
  })

  const [inputValue, setInputValue] = useControlled<string>({
    controlled: inputValueProp,
    default: '',
    name: componentName,
    state: 'inputValue',
  })

  const [isFocused, setIsFocused] = useState(false)
  const [isPristine, setIsPristine] = useState(false)

  const filteredOptions = useFilterOptions({
    isOpen,
    options,
    inputValue:
      !isMulti && value !== null && value === inputValue && isPristine
        ? ''
        : inputValue,
  })

  const setHighlightedIndex = useHighlightIndex(
    {
      id,
      isOpen,
      options: filteredOptions,
      onHighlightedIndexChange,
    },
    inputRef,
    listboxRef,
    highlightedIndexRef,
  )

  const isAvailable = isOpen && filteredOptions?.length > 0
  const isDirty =
    inputValue?.length > 0 || isMulti ? value?.length > 0 : value !== null
  const hasValue = isMulti ? value?.length > 0 : value

  useEffect(
    function handleScrollSelectedOptionIntoView() {
      const valueArray = isMulti ? value : [value]

      if (!isOpen || !listboxRef?.current || valueArray?.length === 0) {
        return
      }

      const valueItem = valueArray?.[0]
      const valueIndex = filteredOptions?.findIndex(option =>
        getOptionSelected(option, valueItem),
      )

      if (valueIndex !== -1) {
        setHighlightedIndex({ index: valueIndex })
      }
    },
    [
      filteredOptions,
      getOptionSelected,
      isMulti,
      isOpen,
      setHighlightedIndex,
      value,
    ],
  )

  const handleResetInputValue = useCallback(
    (event: InteractionEvent, newValue?: string) => {
      let newInputValue: string

      if (isMulti || newValue === null) {
        newInputValue = ''
      } else {
        newInputValue = newValue
      }

      setInputValue(newInputValue)
      onInputValueChange?.(event, newInputValue)
    },
    [isMulti, onInputValueChange, setInputValue],
  )

  const handleOnOpen = useCallback(
    event => {
      if (isOpen) {
        return
      }

      setIsOpen(true)
      setIsPristine(true)
      onOpenChange?.(event)
    },
    [isOpen, onOpenChange, setIsOpen],
  )

  const handleOnClose = useCallback(
    event => {
      if (!isOpen) {
        return
      }

      setIsOpen(false)
      onCloseChange?.(event)
    },
    [isOpen, onCloseChange, setIsOpen],
  )

  const handleToggle = useCallback(
    event => {
      if (isOpen) {
        handleOnClose(event)
      } else {
        handleOnOpen(event)
      }
    },
    [handleOnClose, handleOnOpen, isOpen],
  )

  const handleSetValue = useCallback(
    (event, newValue) => {
      if (value === newValue) {
        return
      }

      onValueChange?.(event, newValue)

      setValue(newValue)
    },
    [onValueChange, setValue, value],
  )

  const handleSetNewValue = useCallback(
    (event: InteractionEvent, option: string) => {
      setHighlightedIndex({ diff: 'reset' })
      let newValue: string | string[] = option

      if (isMulti) {
        newValue = Array.isArray(value) ? value.slice() : []

        const index = newValue?.findIndex(valueItem =>
          getOptionSelected(valueItem, option),
        )

        if (index === -1) {
          if (option) {
            newValue.push(option as string)
          }
        } else {
          newValue.splice(index, 1)
        }
      }

      handleSetValue(event, newValue)
      handleResetInputValue(event, option as string)

      if (closeOnSelect) {
        handleOnClose(event)
      }

      if (blurOnSelect) {
        inputRef?.current?.blur()
      }
    },
    [
      blurOnSelect,
      closeOnSelect,
      getOptionSelected,
      handleOnClose,
      handleResetInputValue,
      handleSetValue,
      isMulti,
      setHighlightedIndex,
      value,
    ],
  )

  const handleOnClear = useCallback(
    (event: InteractionEvent) => {
      if (isClearable) {
        setInputValue('')
        onInputValueChange?.(event, '')

        handleSetValue(event, isMulti ? [] : null)
      }
    },
    [handleSetValue, isClearable, isMulti, onInputValueChange, setInputValue],
  )

  const handleOnKeyDown = useCallback(
    (event: KeyboardEvent) => {
      switch (event.key) {
        case 'PageUp':
          event.preventDefault()
          setHighlightedIndex({ diff: -pageSize, direction: 'previous' })
          handleOnOpen(event)
          break
        case 'PageDown':
          event.preventDefault()
          setHighlightedIndex({ diff: pageSize, direction: 'next' })
          handleOnOpen(event)
          break
        case 'ArrowDown':
          event.preventDefault()
          setHighlightedIndex({ diff: 1, direction: 'next' })
          handleOnOpen(event)
          break
        case 'ArrowUp':
          event.preventDefault()
          setHighlightedIndex({ diff: -1, direction: 'previous' })
          handleOnOpen(event)
          break
        case 'Escape':
          if (isOpen) {
            event.preventDefault()
            event.stopPropagation()

            if (clearOnEscape && (inputValue || hasValue)) {
              handleOnClear(event)
            }

            handleOnClose(event)
          }
          break
        case 'Enter':
          if (event.which === 229) {
            break
          }

          if (highlightedIndexRef?.current !== -1 && isOpen) {
            event.preventDefault()

            const option = filteredOptions?.[highlightedIndexRef?.current]
            handleSetNewValue(event, option)
          }
          break
        case 'Backspace':
          if (isMulti && inputValue === '' && hasValue) {
            const index = value?.length - 1
            const newValue: string[] = (value as string[])?.slice()

            newValue.splice(index, 1)

            handleSetValue(event, newValue)
          }
          break
        default:
      }
    },
    [
      clearOnEscape,
      filteredOptions,
      handleOnClear,
      handleOnClose,
      handleOnOpen,
      handleSetNewValue,
      handleSetValue,
      hasValue,
      inputValue,
      isMulti,
      isOpen,
      pageSize,
      setHighlightedIndex,
      value,
    ],
  )

  function handleOnClick() {
    // Always automatically focus on input when opening
    inputRef?.current?.focus()
  }

  const handleOptionOnClick = useCallback(
    (event: MouseEvent) => {
      const index = Number(
        event.currentTarget.getAttribute('data-option-index'),
      )

      handleSetNewValue(event, filteredOptions[index])
    },
    [filteredOptions, handleSetNewValue],
  )

  const handleOnFocus = useCallback(
    (event: FocusEvent) => {
      setIsFocused(true)

      if (openOnFocus) {
        handleOnOpen(event)
      }
    },
    [handleOnOpen, openOnFocus],
  )

  const handleOnBlur = useCallback(
    (event: FocusEvent) => {
      setIsFocused(false)
      setHighlightedIndex({ diff: 'reset' })

      if (clearOnBlur) {
        handleResetInputValue(event, null)
      }

      handleOnClose(event)
    },
    [clearOnBlur, handleOnClose, handleResetInputValue, setHighlightedIndex],
  )

  const handleOnMouseDown = useCallback(
    (event: MouseEvent) => {
      if (event?.currentTarget?.getAttribute('id') !== id) {
        event.preventDefault()
      }
    },
    [id],
  )

  const handleInputOnMouseDown = useCallback(
    (event: MouseEvent) => {
      if (inputValue === '' || !isOpen) {
        event.stopPropagation()
        handleToggle(event)
      }
    },
    [handleToggle, inputValue, isOpen],
  )

  function handleListboxOnMouseDown(event) {
    event.preventDefault()
  }

  const handleOptionOnMouseOver = useCallback(
    (event: MouseEvent) => {
      setHighlightedIndex({
        index: Number(event?.currentTarget?.getAttribute('data-option-index')),
      })
    },
    [setHighlightedIndex],
  )

  const handleInputOnChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      const newValue = event?.target?.value

      if (inputValue !== newValue) {
        setInputValue(newValue)
        setIsPristine(false)
        onInputValueChange?.(event, newValue)
      }

      if (newValue === '') {
        if (isClearable && !isMulti) {
          handleSetValue(event, null)
        }
      } else {
        handleOnOpen(event)
      }
    },
    [
      handleOnOpen,
      handleSetValue,
      inputValue,
      isClearable,
      isMulti,
      onInputValueChange,
      setInputValue,
    ],
  )

  const handleTagOnDismiss = useCallback(
    (index: number) => event => {
      const newValue = value?.slice() as string[]
      newValue.splice(index, 1)

      handleSetValue(event, newValue)
    },
    [handleSetValue, value],
  )

  const getRootProps = useCallback(
    (): AutocompleteRootProps => ({
      role: 'combobox',
      onClick: handleOnClick,
      onKeyDown: handleOnKeyDown,
      onMouseDown: handleOnMouseDown,

      'aria-owns': isAvailable ? `${id}-listbox` : null,
      'aria-expanded': isAvailable,
    }),
    [handleOnKeyDown, handleOnMouseDown, id, isAvailable],
  )

  const getLabelProps = useCallback(
    (): AutocompleteLabelProps => ({
      id: `${id}-label`,
      htmlFor: id,
    }),
    [id],
  )

  const getInputProps = useCallback(
    (): AutocompleteInputProps => ({
      id,
      ref: inputRef,
      value: inputValue,
      autoComplete: 'none',
      autoCapitalize: 'none',
      spellCheck: false,
      disabled: isDisabled,
      onFocus: handleOnFocus,
      onBlur: handleOnBlur,
      onChange: handleInputOnChange,
      onMouseDown: handleInputOnMouseDown,

      'aria-controls': isAvailable ? `${id}-listbox` : null,
      'aria-autocomplete': 'list' as 'list' | 'both',
      'aria-activedescendant': isOpen ? '' : null,
    }),
    [
      id,
      inputValue,
      isDisabled,
      handleOnFocus,
      handleOnBlur,
      handleInputOnChange,
      handleInputOnMouseDown,
      isAvailable,
      isOpen,
    ],
  )

  const getListboxProps = useCallback(
    (): AutocompleteListboxProps => ({
      id: `${id}-listbox`,
      ref: listboxRef as any,
      role: 'listbox',
      onMouseDown: handleListboxOnMouseDown,

      'aria-labelledby': `${id}-label`,
    }),
    [id],
  )

  const getOptionProps = useCallback(
    ({ index, option }): AutocompleteOptionProps => {
      const valueArray = isMulti ? value : [value]
      const selected =
        valueArray?.length > 0 &&
        !!(valueArray as string[])?.find(valueItem =>
          getOptionSelected<string>(option, valueItem),
        )
      const disabled = getOptionDisabled?.(option)

      return {
        key: index,
        id: `${id}-option-${index}`,
        role: 'option',
        tabIndex: -1,
        onClick: handleOptionOnClick,
        onMouseOver: handleOptionOnMouseOver,

        'data-option-index': index,
        'aria-disabled': disabled,
        'aria-selected': selected,
        ...(selected ? { 'data-option-selected': true } : null),
      }
    },
    [
      getOptionDisabled,
      getOptionSelected,
      handleOptionOnClick,
      handleOptionOnMouseOver,
      id,
      isMulti,
      value,
    ],
  )

  const getClearButtonProps = useCallback(
    (): AutocompleteInputButtonProps => ({
      tabIndex: -1,
      onClick: handleOnClear,
    }),
    [handleOnClear],
  )

  const getPopoverButtonProps = useCallback(
    (): AutocompleteInputButtonProps => ({
      tabIndex: -1,
      disabled: isDisabled,
      onClick: handleToggle,
    }),
    [handleToggle, isDisabled],
  )

  const getTagProps = useCallback(
    ({ index }: { index: number }): AutocompleteTagProps => ({
      onDismiss: handleTagOnDismiss(index),
      'data-tag-index': index,
    }),
    [handleTagOnDismiss],
  )

  return {
    isOpen,
    isAvailable,
    isDirty,
    isFocused,
    value,
    options: filteredOptions,
    getRootProps,
    getLabelProps,
    getInputProps,
    getListboxProps,
    getOptionProps,
    getClearButtonProps,
    getPopoverButtonProps,
    getTagProps,
  }
}
Example #19
Source File: Input.tsx    From oxen-website with GNU General Public License v3.0 4 votes vote down vote up
export function Input(props: InputProps) {
  const {
    className,
    inputClassName,
    type = 'text',
    center = false,
    readonly = false,
    size = 'medium',
    border = 'secondary',
    style,
    prefix,
    duration = true,
    suffix,
    required,
    disabled,
    min,
    max,
    step,
    placeholder = '',
    inputMode = 'text',
    fitHeight = false,
    onKeyDown,
    onMouseUp,
  } = props;

  // Focus
  const inputRef = useRef<HTMLInputElement>(null);
  const setInputFocus = () => {
    if (typeof inputRef !== 'string') {
      inputRef?.current?.focus();
    }
  };

  // Value
  const [value, setValue] = useState('' as string | number);
  const [hasFocus, setHasFocus] = useState(false);

  // Styles
  const fontSize =
    size !== 'medium' && size === 'large' ? 'text-lg' : 'text-sm';

  // Functions
  const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
    const element = event?.target as HTMLInputElement;
    if (element.value === undefined) {
      return;
    }

    // Emails don't support selectionStart
    if (type !== 'email') {
      const caret = element.selectionStart;
      window.requestAnimationFrame(() => {
        element.selectionStart = caret;
        element.selectionEnd = caret;
      });
    }

    if (props.onValueChange) {
      props.onValueChange(element.value);
    }

    setValue(element.value);
  };

  const handleOnBlur = (event: FocusEvent<HTMLInputElement>) => {
    setHasFocus(false);

    if (props.onBlur) {
      props.onBlur(event);
    }
  };

  const handleOnFocus = (event: FocusEvent<HTMLInputElement>) => {
    if (!readonly) {
      setHasFocus(true);
    }

    if (props.onFocus) {
      props.onFocus(event);
    }
  };

  // // Effects
  // useEffect(() => {
  //   if (autofocus) {
  //     setInputFocus();
  //   }
  // }, []);

  return (
    <div
      style={style ?? {}}
      className={classNames(
        'flex',
        'items-center',
        'appearance-none',
        'rounded-lg',
        'w-full',
        // 'bg-white',
        'text-gray-700',
        'leading-tight',
        'focus:outline-black',
        border !== 'none' && 'border-2',
        size === 'small' ? 'px-2' : 'px-4',
        duration && 'duration-300',
        disabled && 'opacity-50 cursor-not-allowed',
        border === 'primary' && 'border-primary',
        border === 'secondary' && hasFocus
          ? `border-primary`
          : 'border-secondary',
        className,
      )}
      onClick={setInputFocus}
    >
      {prefix && (
        <span
          className={classNames(`text-black`, 'flex', 'items-center', 'pr-4')}
        >
          {prefix}
        </span>
      )}

      <input
        className={classNames(
          'bg-transparent',
          'outline-none',
          'flex-1',
          'w-0',
          size === 'large' && 'py-2',
          disabled && 'cursor-not-allowed',
          center && 'text-center',
          fontSize,
          inputClassName,
        )}
        readOnly={readonly}
        type={type}
        ref={inputRef}
        spellCheck={false}
        disabled={disabled}
        required={required}
        placeholder={placeholder}
        value={props.value ?? value}
        step={step}
        min={min}
        max={max}
        onChange={handleOnChange}
        onFocus={handleOnFocus}
        onBlur={handleOnBlur}
        inputMode={inputMode}
        onKeyDown={onKeyDown}
        onMouseUp={onMouseUp}
      ></input>

      {type === 'number' && <div className="h-full bg-green-200"></div>}

      {suffix && (
        <span
          className={classNames(`text-primary`, 'flex', 'items-center', 'pl-4')}
        >
          {suffix}
        </span>
      )}
    </div>
  );
}
Example #20
Source File: VerificationCodeInput.tsx    From design-system with MIT License 4 votes vote down vote up
VerificationCodeInput = (props: VerificationCodeInputProps) => {
  const {
    type = 'number',
    fields = 4,
    placeholder = '_',
    autoFocus = true,
    onComplete,
    onFocus,
    onBlur,
    className,
    value,
    ...rest
  } = props;

  const initialValues = useMemo(() => {
    if (props.value && props.value.length) {
      return props.value.split('');
    }
    return Array(fields).fill('');
  }, []);

  const initialRefs = useMemo(() => {
    return [...Array(fields)].map(() => {
      return React.createRef<HTMLInputElement>();
    });
  }, []);

  const [values, setValues] = useState<string[]>(initialValues);
  const [refs] = useState<Refs>(initialRefs);

  useEffect(() => {
    if (refs[0] && refs[0].current && autoFocus) {
      refs[0].current.focus({ preventScroll: true });
    }
  }, []);

  useEffect(() => {
    const completeValue = values.join('');
    if (onComplete && completeValue.length === fields) {
      onComplete(completeValue);
    }
  }, [values]);

  const onChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
    const index = parseInt(e.target.dataset.id as string, 10);
    const fieldValue = e.target.value;
    let nextRef;
    const newValues = [...values];

    if (!fieldValue) {
      return;
    }

    if (fieldValue.length > 1) {
      let nextIndex = fieldValue.length + index - 1;
      if (nextIndex >= fields) {
        nextIndex = fields - 1;
      }
      nextRef = refs[nextIndex];
      const split = fieldValue.split('');
      split.forEach((item: string, i: number) => {
        const cursor: number = index + i;
        if (cursor < fields) {
          newValues[cursor] = item;
        }
      });
      setValues(newValues);
    } else {
      nextRef = refs[index + 1];
      newValues[index] = fieldValue;
      setValues(newValues);
    }

    if (nextRef && nextRef.current) {
      nextRef.current.focus({ preventScroll: true });
      nextRef.current.select();
    }
  };

  const onFocusHandler = (e: FocusEvent<HTMLInputElement>) => {
    e.target.select();
    e.target.placeholder = '';
    if (onFocus) {
      onFocus(e);
    }
  };

  const onBlurHandler = (e: FocusEvent<HTMLInputElement>) => {
    e.target.placeholder = placeholder;
    if (onBlur) {
      onBlur(e);
    }
  };

  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    const index = parseInt(e.currentTarget.dataset.id as string, 10);
    const prevIndex = index - 1;
    const nextIndex = index + 1;
    const prev = refs[prevIndex];
    const nextRef = refs[nextIndex];
    switch (e.key) {
      case KEY_CODE.backspace: {
        e.preventDefault();
        const vals = [...values];
        if (values[index]) {
          vals[index] = '';
          setValues(vals);
        } else if (prev && prev.current) {
          vals[prevIndex] = '';
          prev.current.focus({ preventScroll: true });
          setValues(vals);
        }
        break;
      }
      case KEY_CODE.left: {
        e.preventDefault();
        if (prev && prev.current) {
          prev.current.focus({ preventScroll: true });
        }
        break;
      }
      case KEY_CODE.right: {
        e.preventDefault();
        if (nextRef && nextRef.current) {
          nextRef.current.focus({ preventScroll: true });
        }
        break;
      }
      case KEY_CODE.up:
      case KEY_CODE.down:
      case KEY_CODE.e:
      case KEY_CODE.E: {
        if (type === 'number') {
          e.preventDefault();
        }
        break;
      }
      default:
        break;
    }
  };

  const wrapperClassNames = (i: number) =>
    classNames(
      {
        'VerificationCodeInput-Input': true,
        'ml-4': i > 0,
      },
      className
    );

  return (
    <div data-test="DesignSystem-VerificationCodeInput" className="VerificationCodeInput">
      {values.map((val: string, index: number) => (
        <Input
          key={index}
          className={wrapperClassNames(index)}
          size="large"
          minWidth="40px"
          value={val}
          placeholder={placeholder}
          onChange={onChangeHandler}
          onKeyDown={onKeyDown}
          onFocus={onFocusHandler}
          onBlur={onBlurHandler}
          data-id={index}
          ref={refs[index]}
          {...rest}
        />
      ))}
    </div>
  );
}
Example #21
Source File: MenuBar.tsx    From ble with Apache License 2.0 4 votes vote down vote up
MenuBar: FunctionComponent = () => {
	const { level, undoManager } = useStore();
	const dispatch = useDispatch();

	function on2StarsChange(value: number): void {
		level.set2StarsTime(value * 1000);
	}
	function on3StarsChange(value: number): void {
		level.set3StarsTime(value * 1000);
	}
	function onNameChange(ev: ChangeEvent<HTMLInputElement>): void {
		level.setName(ev.target.value);
	}

	function onNameFocus(): void {
		undoManager.startGroup();
	}
	function onNameBlur(ev: FocusEvent<HTMLInputElement>): void {
		undoManager.stopGroup();
		ev.target.reportValidity();
	}

	function onStarFocus(): void {
		undoManager.startGroup();
	}
	function onStarBlur(): void {
		undoManager.stopGroup();
	}

	function onUndo(): void {
		dispatch({
			type: 'undo',
		});
	}

	function onRedo(): void {
		dispatch({
			type: 'redo',
		});
	}

	return (
		<Bar>
			<Line>
				<LevelNameInput
					type="text"
					value={level.name}
					onChange={onNameChange}
					onFocus={onNameFocus}
					onBlur={onNameBlur}
					placeholder="Level name"
					required
				/>
				<Table>
					<TableRow
						title={`Finish in ${level.timings[0] / 1000}s or less to get 2 stars`}
					>
						<Text>★★</Text>
						<StarInput
							min={0}
							step={0.001}
							value={level.timings[0] / 1000}
							onChange={on2StarsChange}
							onFocus={onStarFocus}
							onBlur={onStarBlur}
							required
						/>
						<Text>secs</Text>
					</TableRow>
					<TableRow
						title={`Finish in ${level.timings[1] / 1000}s or less to get 3 stars`}
					>
						<Text>★★★</Text>
						<StarInput
							min={0}
							step={0.001}
							value={level.timings[1] / 1000}
							onChange={on3StarsChange}
							onFocus={onStarFocus}
							onBlur={onStarBlur}
							required
						/>
						<Text>secs</Text>
					</TableRow>
				</Table>
			</Line>
			<Line>
				<Button onClick={onUndo} disabled={!undoManager.canUndo}>
					<FontAwesomeIcon icon={faUndo}/>
					&#32;
					Undo
				</Button>
				<Button onClick={onRedo} disabled={!undoManager.canRedo}>
					<FontAwesomeIcon icon={faRedo}/>
					&#32;
					Redo
				</Button>
				<LoadSave/>
				<HomeButton/>
			</Line>
		</Bar>
	);
}
Example #22
Source File: search-items.tsx    From geist-ui with MIT License 4 votes vote down vote up
SearchItems = React.forwardRef<
  SearchItemsRef,
  React.PropsWithChildren<SearchItemsProps>
>(
  (
    { data, onSelect, preventHoverHighlightSync },
    outRef: React.Ref<SearchItemsRef | null>,
  ) => {
    const theme = useTheme()
    const { rect, setRect } = useRect()
    const ref = useRef<HTMLUListElement | null>(null)
    const [displayHighlight, setDisplayHighlight] = useState<boolean>(false)
    useImperativeHandle(outRef, () =>
      Object.assign(ref.current, {
        closeHighlight: () => setDisplayHighlight(false),
      }),
    )

    const hoverHandler = (event: MouseEvent<HTMLButtonElement>) => {
      if (preventHoverHighlightSync) return
      if (!isSearchItem(event.target as HTMLButtonElement)) return
      ;(event.target as HTMLButtonElement).focus()
    }
    const focusHandler = (event: FocusEvent<HTMLButtonElement>) => {
      if (!isSearchItem(event.target as HTMLButtonElement)) return
      setRect(event, () => ref.current)
      setDisplayHighlight(true)
    }
    const blurHandler = () => {
      setDisplayHighlight(false)
    }

    const grouppedResults = useMemo(() => groupResults(data), [data])

    return (
      <ul className="results" role="listbox" ref={ref}>
        <Highlight
          className="results-hover"
          rect={rect}
          visible={displayHighlight}
          activeOpacity={0.5}
        />
        {grouppedResults.map((group) => (
          <li role="presentation" key={group.title}>
            <div className="group-title">{group.title}</div>
            <ul role="group">
              {group.items.map(item => (
                <SearchItem
                  onSelect={onSelect}
                  onMouseOver={hoverHandler}
                  onFocus={focusHandler}
                  onBlur={blurHandler}
                  data={item}
                  key={item.url}
                />
              ))}
            </ul>
          </li>
        ))}
        <style jsx>{`
          .results {
            width: 100%;
            max-height: 300px;
            overflow-y: auto;
            position: relative;
            scroll-behavior: smooth;
            margin-bottom: 0.5rem;
          }
          .results :global(li:before) {
            content: none;
          }
          .group-title {
            color: ${theme.palette.accents_5};
            font-size: 0.75rem;
            text-align: start;
            margin: 0.25rem 0;
          }
          .results:global(div.highlight.results-hover) {
            border-radius: 8px;
          }
        `}</style>
      </ul>
    )
  },
)
Example #23
Source File: ValidatorReport.tsx    From community-repo with GNU General Public License v3.0 4 votes vote down vote up
ValidatorReport = () => {
    const dateFormat = 'yyyy-MM-DD';
    const [backendUrl, setBackendUrl] = useState(process.env.REACT_APP_BACKEND_URL || "http://localhost:3500");
    const [activeValidators, setActiveValidators] = useState([]);
    const [lastBlock, setLastBlock] = useState(0);
    const [stash, setStash] = useState('5EhDdcWm4TdqKp1ew1PqtSpoAELmjbZZLm5E34aFoVYkXdRW');
    const [dateFrom, setDateFrom] = useState(moment().subtract(14, 'd').format(dateFormat));
    const [dateTo, setDateTo] = useState(moment().format(dateFormat));
    const [startBlock, setStartBlock] = useState('' as unknown as number);
    const [endBlock, setEndBlock] = useState('' as unknown as number);
    const [isLoading, setIsLoading] = useState(false);
    const [isModalOpen, setIsModalOpen] = useState(false);
    const [error, setError] = useState(undefined);
    const [currentPage, setCurrentPage] = useState(1);
    const [filterTab, setFilterTab] = useState(0 as number);
    const [columns] = useState(
        [
            { field: 'id', headerName: 'Era', width: 150, sortable: true },
            { field: 'stakeTotal', headerName: 'Total Stake', width: 150, sortable: true },
            { field: 'stakeOwn', headerName: 'Own Stake', width: 150, sortable: true },
            { field: 'points', headerName: 'Points', width: 150, sortable: true },
            { field: 'rewards', headerName: 'Rewards', width: 150, sortable: true },
            { field: 'commission', headerName: 'Commission', width: 150, sortable: true, valueFormatter: (params: ValueFormatterParams) => {
                if (isNaN(params.value as unknown as number)) {
                    return `${params.value}%`
                }
                return `${Number(params.value).toFixed(0)}%`
            }},
            { field: 'blocksCount', headerName: 'Blocks Produced', width: 150, sortable: true },
        ]
    );
    const [report, setReport] = useState({
        pageSize: 0,
        totalCount: 0,
        totalBlocks: 0,
        startEra: -1,
        endEra: -1,
        startBlock: -1,
        endBlock: -1,
        startTime: -1,
        endTime: -1,
        report: [] as unknown as Report[]
    } as unknown as Reports );

    const isDateRange = filterTab === 0;
    const isBlockRange = filterTab === 1;

    useEffect(() => {
        updateChainState()
        const interval = setInterval(() => { updateChainState() }, 10000);
        return () => clearInterval(interval);
    }, []);

    const updateChainState = () => {
        getChainState().then((chainState) => {
            setLastBlock(chainState.finalizedBlockHeight)
            setActiveValidators(chainState.validators.validators)
        })
    }

    const handlePageChange = (params: PageChangeParams) => {
        if (report.totalCount > 0) {
            loadReport(params.page)
        }
    }

    const loadReport = (page: number) => {
        setCurrentPage(page)
        setIsLoading(true)
        const blockParam = isBlockRange && startBlock && endBlock ? `&start_block=${startBlock}&end_block=${endBlock}` : ''
        const dateParam = isDateRange && dateFrom && dateTo ? `&start_time=${moment(dateFrom, dateFormat).format(dateFormat)}&end_time=${moment(dateTo, dateFormat).format(dateFormat)}` : ''
        const apiUrl = `${backendUrl}/validator-report?addr=${stash}&page=${page}${blockParam}${dateParam}`
        axios.get(apiUrl).then((response) => {
            if (response.data.report !== undefined) {
                setReport(response.data);
            }
            setIsLoading(false)
            setError(undefined)
        }).catch((err) => {
            setIsLoading(false)
            setError(err)
        })
    }

    const stopLoadingReport = () => {
        setIsLoading(false)
    }

    const canLoadReport = () => stash && ((isBlockRange && startBlock && endBlock) || (isDateRange && dateFrom && dateTo))
    const startOrStopLoading = () => isLoading ? stopLoadingReport() : loadReport(1)
    const updateStartBlock = (e: { target: { value: unknown; }; }) => setStartBlock((e.target.value as unknown as number));
    const updateEndBlock = (e: { target: { value: unknown; }; }) => setEndBlock((e.target.value as unknown as number));
    const updateDateFrom = (e: { target: { value: unknown; }; }) => setDateFrom((e.target.value as unknown as string))
    const updateDateTo = (e: { target: { value: unknown; }; }) => setDateTo((e.target.value as unknown as string));

    const setCurrentPeriodStartBlock = () => {
        const blocksToEndOfDay = moment().endOf('d').diff(moment(), "seconds") / 6
        const twoWeeksBlocks = (600 * 24 * 14);
        return setStartBlock(lastBlock - twoWeeksBlocks - Number(blocksToEndOfDay.toFixed(0)))
    }

    const setCurrentPeriodEndBlock = () => setEndBlock(lastBlock)

    const getButtonTitle = (isLoading: boolean) => {
        if (isLoading) {
            return (<div style={{ display: 'flex', alignItems: 'center' }}>Stop loading <CircularProgress style={ { color: '#fff', height: 20, width: 20, marginLeft: 12 } } /></div>)
        }
        if (isBlockRange) {
            return startBlock && endBlock ? `Load data between blocks ${startBlock} - ${endBlock}` : 'Load data between blocks'
        }
        if (isDateRange) {
            return dateFrom && dateTo ? `Load data between dates ${dateFrom} - ${dateTo}` : 'Load data between dates'
        }
        return 'Choose dates or blocks range'
    }
    const updateStash = (event: ChangeEvent<{}>, value: string | null, reason: AutocompleteChangeReason, details?: AutocompleteChangeDetails<string> | undefined) => {
        setStash(value || '')
    }
    
    const updateStashOnBlur = (event: FocusEvent<HTMLDivElement> & { target: HTMLInputElement}) => {
        setStash((prev) => prev !== event.target.value ? event.target.value : prev)
    }

    const classes = useStyles();
    return (
        <div className={classes.root}>
            <Container maxWidth="lg">
                <Grid container spacing={2}>
                    <Grid item lg={12}>
                        <div style={{ display: 'flex', justifyContent: 'flex-start' }}>
                            <h1>Validator Report</h1>
                            <Button style={{ display: 'none', fontSize: 12, alignSelf: 'center'}} onClick={() => setIsModalOpen(true)}><Edit style={{ fontSize: 12, alignSelf: 'center'}} /> Backend</Button>
                        </div>
                        <Dialog style={{ minWidth: 275 }} onClose={() => setIsModalOpen(false)} aria-labelledby="simple-dialog-title" open={isModalOpen}>
                            <DialogTitle id="simple-dialog-title">Change Backend URL</DialogTitle>
                            <FormControl style={ { margin: 12 }}>
                                <Select
                                    labelId="backend-url-label"
                                    id="backend-url"
                                    value={backendUrl}
                                    onChange={(e) => setBackendUrl(e.target.value as unknown as string)}
                                >
                                <MenuItem value={'https://validators.joystreamstats.live'}>validators.joystreamstats.live</MenuItem>
                                <MenuItem value={'https://joystream-api.herokuapp.com'}>joystream-api.herokuapp.com</MenuItem>
                                <MenuItem value={'http://localhost:3500'}>localhost:3500</MenuItem>
                                </Select>
                            </FormControl>
                        </Dialog>
                    </Grid>
                    <Grid item xs={12} lg={12}>
                        <Autocomplete
                            freeSolo
                            style={{ width: '100%' }}
                            options={activeValidators}
                            onChange={updateStash}
                            onBlur={updateStashOnBlur}
                            value={stash}
                            renderInput={(params) => <TextField {...params} label="Validator stash address" variant="filled" />} />
                    </Grid>
                    <Grid item xs={12} lg={12}>
                        <Tabs indicatorColor='primary' value={filterTab} onChange={(e: unknown, newValue: number) => setFilterTab(newValue)} aria-label="simple tabs example">
                            <Tab label="Search by date" />
                            <Tab label="Search by blocks" />
                        </Tabs>
                    </Grid>
                    <Grid hidden={!isDateRange} item xs={6} lg={3}>
                        <TextField fullWidth type="date" onChange={updateDateFrom} id="block-start" InputLabelProps={{ shrink: true }} label="Date From" value={dateFrom} variant="filled" />
                    </Grid>
                    <Grid hidden={!isDateRange} item xs={6} lg={3}>
                        <BootstrapButton size='large' style={{ height: 56 }} fullWidth onClick={() => setDateFrom(moment().subtract(2, 'w').format('yyyy-MM-DD'))}>2 weeks from today</BootstrapButton>
                    </Grid>
                    <Grid hidden={!isDateRange} item xs={6} lg={3}>
                        <TextField fullWidth type="date" onChange={updateDateTo} id="block-end" InputLabelProps={{ shrink: true }} label="Date To" value={dateTo} variant="filled" />
                    </Grid>
                    <Grid hidden={!isDateRange} item xs={6} lg={3}>
                        <BootstrapButton size='large' style={{ height: 56 }} fullWidth onClick={() => setDateTo(moment().format('yyyy-MM-DD'))}>Today</BootstrapButton>
                    </Grid>
                    <Grid hidden={!isBlockRange} item xs={6} lg={3}>
                        <TextField fullWidth type="number" onChange={updateStartBlock} id="block-start" label="Start Block" value={startBlock} variant="filled" />
                    </Grid>
                    <Grid hidden={!isBlockRange} item xs={6} lg={3}>
                        <BootstrapButton size='large' style={{ height: 56 }} fullWidth disabled={!lastBlock} onClick={setCurrentPeriodStartBlock}>{lastBlock ? `2 weeks before latest (${lastBlock - (600 * 24 * 14)})` : '2 weeks from latest'}</BootstrapButton>
                    </Grid>
                    <Grid hidden={!isBlockRange} item xs={6} lg={3}>
                        <TextField fullWidth type="number" onChange={updateEndBlock} id="block-end" label="End Block" value={endBlock} variant="filled" />
                    </Grid>
                    <Grid hidden={!isBlockRange} item xs={6} lg={3}>
                        <BootstrapButton size='large' style={{ height: 56 }} fullWidth disabled={!lastBlock} onClick={setCurrentPeriodEndBlock}>{lastBlock ? `Pick latest block (${lastBlock})` : 'Use latest block'}</BootstrapButton>
                    </Grid>
                    <Grid item xs={12} lg={12}>
                        <BootstrapButton size='large' style={{ height: 56 }} fullWidth disabled={!canLoadReport()} onClick={startOrStopLoading}>{getButtonTitle(isLoading)}</BootstrapButton>
                        <Alert style={ error !== undefined ? { marginTop: 12 } : { display: 'none'} } onClose={() => setError(undefined)} severity="error">Error loading validator report, please try again.</Alert>
                    </Grid>
                    <Grid item xs={12} lg={12}>
                        <ValidatorReportCard stash={stash} report={report} />
                    </Grid>
                    <Grid item xs={12} lg={12}>
                        <div style={{ height: 400 }}>
                            <Backdrop className={classes.backdrop} open={isLoading}>
                                <CircularProgress color="inherit" />
                            </Backdrop>
                            <DataGrid 
                                rows={report.report} 
                                columns={columns as unknown as ColDef[]}
                                rowCount={report.totalCount}
                                pagination
                                paginationMode="server"
                                onPageChange={handlePageChange} 
                                pageSize={report.pageSize}
                                rowsPerPageOptions={[]}
                                disableSelectionOnClick
                                page={currentPage}
                                />
                        </div>
                    </Grid>
                </Grid>
            </Container>
        </div>
    )
}
Example #24
Source File: user-row.tsx    From keycaplendar with MIT License 4 votes vote down vote up
UserRow = ({
  delete: deleteFn,
  getUsers,
  user: propsUser,
}: UserRowProps) => {
  const currentUser = useAppSelector(selectUser);

  const allDesigners = useAppSelector(selectAllDesigners);

  const [user, updateUser] = useImmer<UserType>(propsUser);
  const [edited, setEdited] = useState(false);
  const [loading, setLoading] = useState(false);
  const [focused, setFocused] = useState("");

  const keyedUpdate =
    <T extends UserType, K extends keyof T>(key: K, payload: T[K]) =>
    (draft: T) => {
      draft[key] = payload;
    };

  useEffect(() => {
    if (propsUser !== user) {
      updateUser(user);
      setEdited(false);
    }
  }, [propsUser]);

  const handleFocus = (e: FocusEvent<HTMLInputElement>) =>
    setFocused(e.target.name);
  const handleBlur = () => setFocused("");

  const handleCheckboxChange = ({
    target: { checked, name },
  }: ChangeEvent<HTMLInputElement>) => {
    if (arrayIncludes(roles, name)) {
      updateUser(keyedUpdate(name, checked));
      setEdited(true);
    }
  };

  const handleChange = ({
    target: { name, value },
  }: ChangeEvent<HTMLInputElement>) => {
    if (hasKey(user, name)) {
      updateUser(keyedUpdate(name, value));
      setEdited(true);
    }
  };

  const selectValue = <Key extends keyof UserType>(
    prop: Key,
    value: UserType[Key]
  ) => {
    updateUser(keyedUpdate(prop, value));
    setEdited(true);
  };

  const setRoles = () => {
    setLoading(true);
    const setRolesFn = firebase.functions().httpsCallable("setRoles");
    setRolesFn({
      admin: user.admin,
      designer: user.designer,
      editor: user.editor,
      email: user.email,
      nickname: user.nickname,
    }).then((result) => {
      setLoading(false);
      if (
        result.data.designer === user.designer &&
        result.data.editor === user.editor &&
        result.data.admin === user.admin
      ) {
        queue.notify({ title: "Successfully edited user permissions." });
        getUsers();
      } else if (result.data.error) {
        queue.notify({
          title: `Failed to edit user permissions: ${result.data.error}`,
        });
      } else {
        queue.notify({ title: "Failed to edit user permissions." });
      }
    });
  };
  const saveButton = loading ? (
    <CircularProgress />
  ) : (
    <IconButton
      disabled={!edited}
      icon={iconObject(<Save />)}
      onClick={() => {
        if (edited) {
          setRoles();
        }
      }}
    />
  );
  const deleteButton =
    user.email === currentUser.email ||
    user.email === "[email protected]" ? null : (
      <IconButton
        icon={iconObject(<Delete />)}
        onClick={() => deleteFn(user)}
      />
    );
  return (
    <DataTableRow>
      <DataTableCell>
        <Avatar name={user.displayName} size="large" src={user.photoURL} />
      </DataTableCell>
      <DataTableCell>{user.displayName}</DataTableCell>
      <DataTableCell>{truncate(user.email, 20)}</DataTableCell>
      <DataTableCell>
        {DateTime.fromISO(user.dateCreated).toFormat(
          `HH:mm d'${ordinal(DateTime.fromISO(user.dateCreated).day)}' MMM yyyy`
        )}
      </DataTableCell>
      <DataTableCell>
        {DateTime.fromISO(user.lastSignIn).toFormat(
          `HH:mm d'${ordinal(DateTime.fromISO(user.lastSignIn).day)}' MMM yyyy`
        )}
      </DataTableCell>
      <DataTableCell>
        {DateTime.fromISO(user.lastActive).toFormat(
          `HH:mm d'${ordinal(DateTime.fromISO(user.lastActive).day)}' MMM yyyy`
        )}
      </DataTableCell>
      <DataTableCell>
        <MenuSurfaceAnchor>
          <TextField
            className="nickname"
            name="nickname"
            onBlur={handleBlur}
            onChange={handleChange}
            onFocus={handleFocus}
            outlined
            value={user.nickname}
          />
          <Autocomplete
            array={allDesigners}
            minChars={2}
            open={focused === "nickname"}
            prop="nickname"
            query={user.nickname}
            select={(prop, item) =>
              hasKey(user, prop) && selectValue(prop, item)
            }
          />
        </MenuSurfaceAnchor>
      </DataTableCell>
      <DataTableCell hasFormControl>
        <Checkbox
          checked={user.designer}
          name="designer"
          onChange={handleCheckboxChange}
        />
      </DataTableCell>
      <DataTableCell hasFormControl>
        <Checkbox
          checked={user.editor}
          disabled={
            user.email === currentUser.email ||
            user.email === "[email protected]"
          }
          name="editor"
          onChange={handleCheckboxChange}
        />
      </DataTableCell>
      <DataTableCell hasFormControl>
        <Checkbox
          checked={user.admin}
          disabled={
            user.email === currentUser.email ||
            user.email === "[email protected]"
          }
          name="admin"
          onChange={handleCheckboxChange}
        />
      </DataTableCell>
      <DataTableCell hasFormControl>{saveButton}</DataTableCell>
      <DataTableCell hasFormControl>{deleteButton}</DataTableCell>
    </DataTableRow>
  );
}
Example #25
Source File: user-card.tsx    From keycaplendar with MIT License 4 votes vote down vote up
UserCard = ({
  delete: deleteFn,
  getUsers,
  user: propsUser,
}: UserCardProps) => {
  const device = useAppSelector(selectDevice);

  const currentUser = useAppSelector(selectUser);

  const allDesigners = useAppSelector(selectAllDesigners);
  const [user, updateUser] = useImmer<UserType>(propsUser);
  const [edited, setEdited] = useState(false);
  const [loading, setLoading] = useState(false);
  const [focused, setFocused] = useState("");

  const keyedUpdate =
    <T extends UserType, K extends keyof T>(key: K, payload: T[K]) =>
    (draft: T) => {
      draft[key] = payload;
    };

  useEffect(() => {
    if (propsUser !== user) {
      updateUser(user);
      setEdited(false);
    }
  }, [propsUser]);

  const handleFocus = (e: FocusEvent<HTMLInputElement>) =>
    setFocused(e.target.name);
  const handleBlur = () => setFocused("");

  const handleChange = ({
    target: { name, value },
  }: ChangeEvent<HTMLInputElement>) => {
    if (hasKey(user, name)) {
      updateUser(keyedUpdate(name, value));
      setEdited(true);
    }
  };
  const selectValue = <Key extends keyof UserType>(
    prop: Key,
    value: UserType[Key]
  ) => {
    updateUser(keyedUpdate(prop, value));
    setEdited(true);
  };
  const toggleRole = (role: typeof roles[number]) => {
    if (roles.includes(role)) {
      updateUser((user) => {
        user[role] = !user[role];
      });
      setEdited(true);
    }
  };

  const setRoles = () => {
    setLoading(true);
    const setRolesFn = firebase.functions().httpsCallable("setRoles");
    setRolesFn({
      admin: user.admin,
      designer: user.designer,
      editor: user.editor,
      email: user.email,
      nickname: user.nickname,
    }).then((result) => {
      setLoading(false);
      if (
        result.data.designer === user.designer &&
        result.data.editor === user.editor &&
        result.data.admin === user.admin
      ) {
        queue.notify({ title: "Successfully edited user permissions." });
        getUsers();
      } else if (result.data.error) {
        queue.notify({
          title: `Failed to edit user permissions: ${result.data.error}`,
        });
      } else {
        queue.notify({ title: "Failed to edit user permissions." });
      }
    });
  };

  const saveButton = loading ? (
    <CircularProgress />
  ) : (
    <CardActionIcon
      disabled={!edited}
      icon={iconObject(<Save />)}
      onClick={() => {
        if (edited) {
          setRoles();
        }
      }}
    />
  );
  const deleteButton =
    user.email === currentUser.email ||
    user.email === "[email protected]" ? null : (
      <IconButton
        icon={iconObject(<Delete />)}
        onClick={() => deleteFn(user)}
      />
    );
  return (
    <Card className="user">
      <List nonInteractive>
        <ListItem className="three-line" ripple={false}>
          <Avatar
            className="mdc-list-item__graphic"
            size="xlarge"
            src={user.photoURL}
          />
          <ListItemText>
            <div className="overline">{user.nickname}</div>
            <ListItemPrimaryText>{user.displayName}</ListItemPrimaryText>
            <ListItemSecondaryText>{user.email}</ListItemSecondaryText>
          </ListItemText>
          <ListItemMeta>{deleteButton}</ListItemMeta>
        </ListItem>
      </List>
      <CollapsibleList
        handle={<SimpleListItem metaIcon="expand_more" text="Account dates" />}
      >
        <List nonInteractive twoLine>
          <ListItem ripple={false}>
            <ListItemText>
              <ListItemPrimaryText>Date created</ListItemPrimaryText>
              <ListItemSecondaryText>
                {DateTime.fromISO(user.dateCreated).toFormat(
                  `HH:mm d'${ordinal(
                    DateTime.fromISO(user.dateCreated).day
                  )}' MMM yyyy`
                )}
              </ListItemSecondaryText>
            </ListItemText>
          </ListItem>
          <ListItem ripple={false}>
            <ListItemText>
              <ListItemPrimaryText>Last signed in</ListItemPrimaryText>
              <ListItemSecondaryText>
                {DateTime.fromISO(user.lastSignIn).toFormat(
                  `HH:mm d'${ordinal(
                    DateTime.fromISO(user.lastSignIn).day
                  )}' MMM yyyy`
                )}
              </ListItemSecondaryText>
            </ListItemText>
          </ListItem>
          <ListItem ripple={false}>
            <ListItemText>
              <ListItemPrimaryText>Last active</ListItemPrimaryText>
              <ListItemSecondaryText>
                {DateTime.fromISO(user.lastActive).toFormat(
                  `HH:mm d'${ordinal(
                    DateTime.fromISO(user.lastActive).day
                  )}' MMM yyyy`
                )}
              </ListItemSecondaryText>
            </ListItemText>
          </ListItem>
        </List>
      </CollapsibleList>
      <div className="text-field-container">
        <MenuSurfaceAnchor>
          <TextField
            className="nickname"
            label="Nickname"
            name="nickname"
            onBlur={handleBlur}
            onChange={handleChange}
            onFocus={handleFocus}
            outlined
            value={user.nickname}
          />
          <Autocomplete
            array={allDesigners}
            minChars={2}
            open={focused === "nickname"}
            prop="nickname"
            query={user.nickname}
            select={(prop, item) =>
              hasKey(user, prop) && selectValue(prop, item)
            }
          />
        </MenuSurfaceAnchor>
      </div>
      <CardActions>
        <CardActionButtons>
          <SegmentedButton toggle>
            {roles.map((role) => (
              <SegmentedButtonSegment
                key={role}
                disabled={
                  (user.email === currentUser.email ||
                    user.email === "[email protected]") &&
                  role !== "designer"
                }
                icon={
                  device === "desktop" && hasKey(userRoleIcons, role)
                    ? userRoleIcons[role]
                    : null
                }
                label={role}
                onClick={() => {
                  toggleRole(role);
                }}
                selected={hasKey(user, role) && !!user[role]}
              />
            ))}
          </SegmentedButton>
        </CardActionButtons>
        <CardActionIcons>{saveButton}</CardActionIcons>
      </CardActions>
    </Card>
  );
}