framer-motion#useReducedMotion TypeScript Examples

The following examples show how to use framer-motion#useReducedMotion. 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: LinearProgress.tsx    From chroma-react with MIT License 6 votes vote down vote up
LinearProgress: React.FC<LinearProgressProps> = ({
  className,
  variant = 'determinate',
  value = 0,
  ...rootProps
}) => {
  const classes = useStyles({});

  const shouldReduceMotion = useReducedMotion();

  return (
    <div
      className={clsx(classes.root, className)}
      role="progressbar"
      aria-valuenow={variant === 'determinate' ? value : undefined}
      aria-valuemin={variant === 'determinate' ? 0 : undefined}
      aria-valuemax={variant === 'determinate' ? 100 : undefined}
      {...rootProps}
    >
      <div
        className={clsx(
          classes.bar,
          variant === 'indeterminate' && classes.indeterminate,
          shouldReduceMotion && classes.indeterminateReduced
        )}
        style={
          variant === 'determinate'
            ? { transform: `translateX(-${100 - value}%)` }
            : undefined
        }
      />
    </div>
  );
}
Example #2
Source File: parallax.tsx    From samuelkraft-next with MIT License 6 votes vote down vote up
Parallax = ({ children, offset = 50, clampInitial, clampFinal }: ParallaxProps): JSX.Element => {
  const prefersReducedMotion = useReducedMotion()
  const [elementTop, setElementTop] = useState(0)
  const [clientHeight, setClientHeight] = useState(0)
  const ref = useRef(null)

  const { scrollY } = useViewportScroll()

  const initial = elementTop - clientHeight
  const final = elementTop + offset

  const yRange = useTransform(scrollY, [initial, final], [clampInitial ? 0 : offset, clampFinal ? 0 : -offset])
  const y = useSpring(yRange, { stiffness: 400, damping: 90 })

  useLayoutEffect(() => {
    const element = ref.current
    const onResize = () => {
      setElementTop(element.getBoundingClientRect().top + window.scrollY || window.pageYOffset)
      setClientHeight(window.innerHeight)
    }
    onResize()
    window.addEventListener('resize', onResize)
    return () => window.removeEventListener('resize', onResize)
  }, [ref])

  // Don't parallax if the user has "reduced motion" enabled
  if (prefersReducedMotion) {
    return <>{children}</>
  }

  return (
    <motion.div ref={ref} style={{ y }}>
      {children}
    </motion.div>
  )
}
Example #3
Source File: Popover.tsx    From chroma-react with MIT License 5 votes vote down vote up
Popover: React.FC<PopoverProps> = ({
  'aria-label': ariaLabel,
  anchorElement,
  children,
  className,
  gutter,
  placement = 'bottom',
  title,
  usePortal = false,
  unstable_offset,
  ...rootProps
}) => {
  const classes = useStyles({});

  const shouldReduceMotion = useReducedMotion();

  const popover = usePopoverState({ placement, gutter, unstable_offset });

  return (
    <>
      <PopoverDisclosure {...popover} {...anchorElement.props}>
        {(disclosureProps) =>
          React.cloneElement(
            React.Children.only(anchorElement),
            disclosureProps
          )
        }
      </PopoverDisclosure>
      <ConditionalWrapper
        condition={usePortal}
        wrapper={(children: React.ReactNode) => <Portal>{children}</Portal>}
      >
        <ReakitPopover
          {...popover}
          aria-label={ariaLabel}
          as={motion.div}
          className={clsx(classes.root, className)}
          aria-hidden={popover.visible ? undefined : 'true'}
          animate={
            popover.visible
              ? shouldReduceMotion
                ? { opacity: 1 }
                : {
                    opacity: 1,
                    transition: { duration: 0.3 },
                  }
              : shouldReduceMotion
              ? { opacity: 0 }
              : {
                  opacity: 0,
                  transition: { duration: 0.1 },
                }
          }
          {...rootProps}
        >
          {!!title && (
            <Text className={classes.title} weight="bold" size="subbody">
              {title}
            </Text>
          )}
          {typeof children === 'function'
            ? children({ popover } as PopoverRenderProps)
            : children}
        </ReakitPopover>
      </ConditionalWrapper>
    </>
  );
}
Example #4
Source File: Tooltip.tsx    From chroma-react with MIT License 5 votes vote down vote up
Tooltip: React.FC<TooltipProps> = ({
  children,
  className,
  defaultVisible = false,
  placement = 'bottom',
  title = '',
  gutter,
  ...rootProps
}) => {
  const classes = useStyles({});

  const shouldReduceMotion = useReducedMotion();

  const tooltip = useTooltipState({
    placement,
    visible: defaultVisible,
    gutter,
  });

  React.useEffect(() => {
    tooltip.place(placement);
  }, [placement, tooltip]);

  return (
    <>
      <TooltipReference {...tooltip}>
        {(referenceProps) =>
          React.cloneElement(React.Children.only(children), referenceProps)
        }
      </TooltipReference>
      <Portal>
        <ReakitTooltip
          {...tooltip}
          as={motion.div}
          className={clsx(classes.root, className)}
          animate={
            tooltip.visible
              ? shouldReduceMotion
                ? { opacity: 1 }
                : {
                    opacity: 1,
                    transition: { delay: 0.75 },
                  }
              : {
                  opacity: 0,
                }
          }
          {...rootProps}
        >
          {title}
        </ReakitTooltip>
      </Portal>
    </>
  );
}
Example #5
Source File: Parallax.tsx    From vignette-web with MIT License 5 votes vote down vote up
Parallax = ({
  children,
  id,
  offset = 30,
  className,
  fadeIn,
}: ParallaxProps): JSX.Element => {
  const prefersReducedMotion = useReducedMotion()
  const [elementTop, setElementTop] = useState(0)
  const [clientHeight, setClientHeight] = useState(0)
  const ref = useRef<HTMLDivElement>(null)

  const { scrollY } = useViewportScroll()

  const initial = elementTop - clientHeight
  const final = elementTop + offset

  const router = useRouter()

  const yRange = useTransform(scrollY, [initial, final], [offset, -offset], {
    clamp: true,
  })
  const y = useSpring(yRange, { stiffness: 400, damping: 90 })

  useEffect(() => {
    const element = ref.current
    const onResize = () => {
      if (element) {
        setElementTop(
          element.getBoundingClientRect().top + window.scrollY ||
            window.pageYOffset,
        )
      }

      setClientHeight(window.innerHeight)
    }
    onResize()
    window.addEventListener(`resize`, onResize)
    return () => window.removeEventListener(`resize`, onResize)
  }, [ref, router.pathname])

  // Don't parallax if the user has "reduced motion" enabled
  if (prefersReducedMotion) {
    return <>{children}</>
  }

  return (
    <motion.div
      id={id}
      className={className}
      ref={ref}
      style={{ y }}
      transition={fadeIn ? { delay: 0.15, duration: 0.3 } : {}}
      initial={fadeIn && { opacity: 0 }}
      whileInView={fadeIn ? { opacity: 1 } : {}}
      viewport={{ once: true }}
    >
      {children}
    </motion.div>
  )
}
Example #6
Source File: Menu.tsx    From chroma-react with MIT License 4 votes vote down vote up
Menu: React.FC<MenuProps> = ({
  'aria-label': ariaLabel,
  anchorElement,
  children,
  className,
  gutter,
  items,
  placement = 'bottom',
  title,
  usePortal = false,
  ...rootProps
}) => {
  const classes = useStyles({});

  const shouldReduceMotion = useReducedMotion();

  const menu = useMenuState({ placement, gutter });

  const handleStopPropagation = (e: React.SyntheticEvent) =>
    e.stopPropagation();

  return (
    <>
      <MenuButton
        {...menu}
        {...anchorElement.props}
        onClick={handleStopPropagation}
      >
        {(disclosureProps) =>
          React.cloneElement(
            React.Children.only(anchorElement),
            disclosureProps
          )
        }
      </MenuButton>
      <ConditionalWrapper
        condition={usePortal}
        wrapper={(children: React.ReactNode) => <Portal>{children}</Portal>}
      >
        <ReakitMenu
          {...menu}
          as={motion.div}
          aria-label={ariaLabel}
          className={clsx(classes.root, className)}
          animate={
            menu.visible
              ? shouldReduceMotion
                ? { opacity: 1 }
                : {
                    opacity: 1,
                    transition: { duration: 0.3 },
                  }
              : shouldReduceMotion
              ? { opacity: 0 }
              : {
                  opacity: 0,
                  transition: { duration: 0.1 },
                }
          }
          {...rootProps}
        >
          {!!title && (
            <Text className={classes.title} weight="bold" size="subbody">
              {title}
            </Text>
          )}
          {items &&
            items.map((item, i) =>
              isMenuItemElement(item) ? (
                <ReakitMenuItem
                  {...menu}
                  {...item.props}
                  key={`item-${i}`}
                  onClick={(e: any) => {
                    menu.hide();
                    item.props.onClick && item.props.onClick(e);
                  }}
                >
                  {(itemProps) =>
                    React.cloneElement(React.Children.only(item), itemProps)
                  }
                </ReakitMenuItem>
              ) : (
                item
              )
            )}
        </ReakitMenu>
      </ConditionalWrapper>
    </>
  );
}
Example #7
Source File: Modal.tsx    From chroma-react with MIT License 4 votes vote down vote up
Content = React.forwardRef<HTMLDivElement, ModalProps>(
  (
    {
      actions,
      className,
      children,
      contentClassName,
      fullWidth,
      justifyActions,
      isFormContent,
      onFormSubmit,
      onClick,
      onDismiss,
      customHeader,
      size,
      title,
      poses = {},
      ...rootProps
    },
    ref
  ) => {
    const classes = useStyles({});
    const shouldReduceMotion = useReducedMotion();
    const poseVariants = {
      init: poses.init || { y: 50, scale: 0.3 },
      open: poses.open || { y: 0, scale: 1 },
      exit: poses.exit || { scale: 0.5, transition: { duration: 0.15 } },
    };

    return (
      <motion.div
        className={clsx(
          classes.content,
          fullWidth && classes.contentFullWidth,
          className
        )}
        role="dialog"
        aria-modal="true"
        aria-describedby={ariaDescribedBy}
        aria-labelledby={ariaLabelledBy}
        tabIndex={-1}
        onClick={composeEventHandlers([
          onClick,
          (event) => {
            event.stopPropagation();
          },
        ])}
        ref={ref}
        variants={poseVariants}
        initial={shouldReduceMotion ? {} : 'init'}
        animate={shouldReduceMotion ? {} : 'open'}
        exit={shouldReduceMotion ? {} : 'exit'}
        {...rootProps}
      >
        {customHeader ? (
          customHeader
        ) : (
          <div
            id={ariaLabelledBy}
            className={clsx(classes.modalHeader, classes.verticalPadding)}
          >
            {!!title && (
              <Text size="subbody" weight="bold">
                {title}
              </Text>
            )}
            <IconButton
              aria-label="Close open modal"
              icon={X}
              size={0}
              tabIndex={0}
              onClick={onDismiss}
              paddingRight={0}
            />
          </div>
        )}
        <ConditionalWrapper
          condition={Boolean(isFormContent && onFormSubmit)}
          wrapper={(children: React.ReactNode) => (
            <form onSubmit={onFormSubmit}>{children}</form>
          )}
        >
          {children && (
            <div
              id={ariaDescribedBy}
              className={clsx(
                classes.modalChildrenContainer,
                classes.verticalPadding,
                {
                  [classes.contentSize0]: size === 0,
                  [classes.contentSize1]: size === 1,
                },
                contentClassName
              )}
            >
              {children}
            </div>
          )}
          {!!actions && (
            <ModalActions
              className={clsx(classes.modalActions, classes.verticalPadding)}
              justify={justifyActions}
            >
              {actions}
            </ModalActions>
          )}
        </ConditionalWrapper>
      </motion.div>
    );
  }
)
Example #8
Source File: ComboBox.tsx    From chroma-react with MIT License 4 votes vote down vote up
ComboBox: React.FC<ComboBoxProps> = ({
  ['aria-label']: ariaLabel,
  children,
  className,
  color = 'default',
  errorMessage,
  fullWidth,
  hasError,
  helpMessage,
  id,
  label,
  secondaryLabel,
  onChange,
  placeholder,
  placement,
  popoverAriaLabel,
  selectedOptionDisplay,
  value,
  ...rootProps
}) => {
  const classes = useStyles({});
  const popover = usePopoverState({ placement });
  const rover = useRoverState({ loop: true, orientation: 'vertical' });
  const shouldReduceMotion = useReducedMotion();

  const buttonRef = React.useRef<any>();

  // This width is used to determine the popover width when open,
  // as well as setting the `maxWidth` of the text inside to ensure
  // the element size doesn't shift when the selected option text
  // gets long.
  const [width, setWidth] = React.useState<number>(50);

  const [internalSelections, setInternalSelections] = React.useState<Array<
    SelectOptionProps
  > | null>(null);

  // A uniqueId is required to wire up aria-attributes
  const [uniqueId] = React.useState<string>(
    () => id || name || generateUniqueId('combobox-')
  );

  // Update the opened popover width when the window width changes
  // TODO: Review the best way to handle this without breaking
  // eslint-disable-next-line react-hooks/exhaustive-deps
  React.useEffect(() => {
    setWidth(buttonRef.current.getBoundingClientRect().width);
  });

  // Determine which of the child select option values are matches based on
  // the "value" prop.  If a select option value is included in the "value",
  // then we add it to the list of internal select options (which keep
  // track of which options are selected to apply classes)
  React.useEffect(() => {
    const matches: any = React.Children.toArray(children).filter((child) => {
      if (!React.isValidElement(child)) {
        return null;
      }

      return value?.includes(child.props.value);
    });

    if (matches.length === 0) {
      setInternalSelections([]);
      return;
    }

    setInternalSelections(matches.map((m: any) => m.props));
  }, [children, value]);

  const handleOptionSelected = (optionValue: string, meta: any) => {
    const isAlreadySelected = internalSelections?.find(
      (s) => s.value === optionValue
    );

    if (isAlreadySelected) {
      // Filter out the option the user selected from our list of internal selections
      // (so that it's removed), and then pass the values and meta arrays back
      // to the consumer.
      const filteredSelections =
        internalSelections?.filter((s) => s.value !== optionValue) || [];
      onChange?.(
        [...filteredSelections?.map((s) => s.value)],
        [...filteredSelections?.map((s) => s.meta)]
      );
      return;
    }

    // We are adding a selected option, so we collect all of the values
    // and meta arrays, add in our new selection, and return the updated
    // list to the consumer
    const valueOptions = internalSelections?.map((s) => s.value) || [];
    const metaOptions = internalSelections?.map((s) => s.meta) || [];
    onChange?.([...valueOptions, optionValue], [...metaOptions, meta]);
  };

  if (!label && !ariaLabel && process.env.NODE_ENV === 'development') {
    throw new Error(
      'If a "label" is not provided to ComboBox, please provide "aria-label".'
    );
  }

  return (
    <div className={clsx(classes.root, className)}>
      <label
        aria-hidden="true"
        className={clsx(
          classes.label,
          color === 'inverse' && classes.labelInverse,
          !label && ariaLabel && classes.srOnly
        )}
        htmlFor={uniqueId}
      >
        {label || ariaLabel}
        {secondaryLabel ? (
          <span
            className={clsx(
              classes.labelSecondary,
              color === 'inverse' && classes.labelInverse
            )}
          >
            {secondaryLabel}
          </span>
        ) : null}
      </label>
      <PopoverDisclosure
        className={clsx(
          classes.button,
          classes.comboxBoxOverflow,
          hasError && classes.buttonError,
          fullWidth && classes.buttonFullWidth,
          {
            [classes.buttonInverse]: color === 'inverse',
          }
        )}
        ref={buttonRef}
        aria-describedby={buildDescribedBy({
          hasError,
          hasHelpMessage: !!helpMessage,
          uniqueId,
        })}
        id={uniqueId}
        {...popover}
        {...rootProps}
      >
        <>
          {internalSelections?.length === 0 && (
            <motion.div
              initial={{ opacity: 0 }}
              animate={{
                opacity: 1,
                transition: { duration: 0.2, ease: 'easeIn' },
              }}
            >
              <Text
                className={clsx(
                  classes.buttonText,
                  classes.placeholderText,
                  classes.comboBoxPlaceholder
                )}
                size="subbody"
              >
                {placeholder}
              </Text>
            </motion.div>
          )}
          {internalSelections &&
            internalSelections?.length > 0 &&
            !selectedOptionDisplay && (
              <Text
                className={clsx(classes.buttonText, classes.chipList)}
                size="subbody"
                style={fullWidth ? {} : { maxWidth: width - 42 }}
              >
                {internalSelections?.map((selectedOption, index) => (
                  <motion.span
                    key={index}
                    className={classes.chip}
                    initial={
                      shouldReduceMotion
                        ? { opacity: 0 }
                        : { opacity: 0, y: -8 }
                    }
                    animate={
                      shouldReduceMotion
                        ? { opacity: 1 }
                        : {
                            opacity: 1,
                            transition: { duration: 0.2, ease: 'easeOut' },
                            y: 0,
                          }
                    }
                  >
                    {selectedOption.title}
                  </motion.span>
                ))}
              </Text>
            )}
          {internalSelections &&
            internalSelections?.length > 0 &&
            selectedOptionDisplay && (
              <Text className={classes.buttonText} size="subbody">
                {selectedOptionDisplay(internalSelections)}
              </Text>
            )}
          <div className={classes.buttonArrowContainer} role="presentation">
            <ChevronDown
              className={clsx(
                classes.arrowIcon,
                popover.visible && classes.rotate
              )}
              aria-hidden
              role="img"
              width={18}
              height={18}
            />
          </div>
        </>
      </PopoverDisclosure>
      {helpMessage && (
        <FormHelpMessage
          className={classes.message}
          color={color}
          rootElementId={uniqueId}
          describedById={helpFor(uniqueId)}
        >
          {helpMessage}
        </FormHelpMessage>
      )}
      {hasError && (
        <FormErrorMessage
          className={classes.message}
          color={color}
          rootElementId={uniqueId}
          describedById={errorFor(uniqueId)}
        >
          {errorMessage}
        </FormErrorMessage>
      )}
      {/*
        A few things here:

        1) We want to always portal our select menu results so we don't
           run into weird layout issues (rendering inside Popover or Modal)
        2) We need to trap focus inside of the menu when it is open - <FocusLock /> handles this for us
        3) We need keyboard support via <Rover />

        Reference for #1 & #2: https://github.com/reakit/reakit/issues/566
      */}
      <Portal>
        <FocusLock>
          <ReakitPopover
            aria-label={label || ariaLabel || popoverAriaLabel}
            className={classes.popover}
            {...popover}
            style={{ width }}
            as={motion.div}
            animate={popover.visible ? 'open' : 'closed'}
            variants={
              shouldReduceMotion ? popoverVariantsReduced : popoverVariants
            }
          >
            <motion.ul
              className={classes.ul}
              variants={
                shouldReduceMotion
                  ? listMotionVariantsReduced
                  : listMotionVariants
              }
              role="listbox"
              aria-multiselectable={true}
            >
              {React.Children.map(children, (child) => {
                if (!React.isValidElement(child)) {
                  return null;
                }

                const option: React.ReactElement<SelectOptionProps> = child;
                return (
                  <Rover
                    {...rover}
                    aria-selected={value?.includes(option?.props?.value)}
                    className={classes.option}
                    as={motion.li}
                    disabled={child?.props?.disabled}
                    role="option"
                    value={option?.props?.value}
                    variants={
                      shouldReduceMotion
                        ? listItemMotionVariantsReduced
                        : listItemMotionVariants
                    }
                    onClick={() =>
                      handleOptionSelected(
                        option?.props?.value,
                        option?.props?.meta
                      )
                    }
                  >
                    {React.cloneElement<SelectOptionProps>(option, {
                      isChecked: value?.includes(option?.props?.value),
                      ...option.props,
                    })}
                  </Rover>
                );
              })}
            </motion.ul>
          </ReakitPopover>
        </FocusLock>
      </Portal>
    </div>
  );
}
Example #9
Source File: Select.tsx    From chroma-react with MIT License 4 votes vote down vote up
Select: React.FC<SelectProps> = ({
  ['aria-label']: ariaLabel,
  children,
  className,
  color = 'default',
  errorMessage,
  fullWidth,
  hasError,
  helpMessage,
  icon: Icon,
  id,
  label,
  secondaryLabel,
  onChange,
  placeholder,
  placement,
  popoverAriaLabel,
  selectedOptionDisplay,
  tooltipMessage,
  value,
  ...rootProps
}) => {
  const classes = useStyles({});
  const popover = usePopoverState({ placement });
  const shouldReduceMotion = useReducedMotion();

  const buttonRef = React.useRef<any>(); // TODO: Need to type this properly...
  const [width, setWidth] = React.useState<number>(50);

  const [
    internalSelection,
    setInternalSelection,
  ] = React.useState<SelectOptionProps | null>(null);

  const [uniqueId] = React.useState<string>(
    () => id || name || generateUniqueId('select-')
  );

  const rover = useRoverState({ loop: true, orientation: 'vertical' });

  // Update the opened popover width any time the button width changes
  // TODO: Review the best way to handle this without breaking
  // eslint-disable-next-line react-hooks/exhaustive-deps
  React.useEffect(() => {
    setWidth(buttonRef.current.getBoundingClientRect().width);
  });

  React.useEffect(() => {
    // TODO: Need to type this properly as well...
    const match: any = React.Children.toArray(children).find((child) => {
      if (!React.isValidElement(child)) {
        return null;
      }

      if (isHeadingElement(child)) {
        return null;
      }

      return child.props.value === value;
    });

    // If there is no match, set internalSelection to null so placeholder will be shown
    setInternalSelection(match ? match.props : null);
  }, [children, value]);

  const hidePopover = popover.hide;
  const handleOptionSelected = React.useCallback(
    (optionValue: string, meta: any) => {
      hidePopover();
      onChange?.(optionValue, meta);
    },
    [hidePopover, onChange]
  );

  if (!label && !ariaLabel && process.env.NODE_ENV === 'development') {
    throw new Error(
      'If a "label" is not provided to Select, please provide "aria-label".'
    );
  }

  return (
    <div className={clsx(classes.root, className)}>
      <label
        aria-hidden="true"
        className={clsx(
          classes.label,
          color === 'inverse' && classes.labelInverse,
          !label && ariaLabel && classes.srOnly
        )}
        htmlFor={uniqueId}
      >
        {label || ariaLabel}
        {!!Icon && tooltipMessage && (
          <Tooltip title={tooltipMessage}>
            <span className={classes.tooltipContainer}>
              <Icon
                className={clsx(
                  classes.labelIcon,
                  color === 'inverse' && classes.labelIconInverse
                )}
                width={16}
                height={16}
                role="img"
                aria-hidden
              />
            </span>
          </Tooltip>
        )}
        {secondaryLabel ? (
          <span
            className={clsx(
              classes.labelSecondary,
              color === 'inverse' && classes.labelInverse
            )}
          >
            {secondaryLabel}
          </span>
        ) : null}
      </label>
      <PopoverDisclosure
        className={clsx(
          classes.button,
          hasError && classes.buttonError,
          fullWidth && classes.buttonFullWidth,
          {
            [classes.buttonInverse]: color === 'inverse',
          }
        )}
        ref={buttonRef}
        aria-describedby={buildDescribedBy({
          hasError,
          hasHelpMessage: !!helpMessage,
          uniqueId,
        })}
        id={uniqueId}
        {...popover}
        {...rootProps}
      >
        <>
          {!internalSelection && (
            <Text
              className={clsx(classes.placeholderText, classes.buttonText)}
              size="subbody"
              {...getTestProps(testIds.placeholderText)}
            >
              {placeholder}
            </Text>
          )}
          {internalSelection && !selectedOptionDisplay && (
            <Text className={classes.buttonText} size="subbody">
              {internalSelection.title}
            </Text>
          )}
          {internalSelection && selectedOptionDisplay && (
            <Text className={classes.buttonText} size="subbody">
              {selectedOptionDisplay(internalSelection)}
            </Text>
          )}
          <div className={classes.buttonArrowContainer} role="presentation">
            <ChevronDown
              className={clsx(
                classes.arrowIcon,
                popover.visible && classes.rotate
              )}
              aria-hidden
              role="img"
              width={18}
              height={18}
            />
          </div>
        </>
      </PopoverDisclosure>
      {helpMessage && (
        <FormHelpMessage
          className={classes.message}
          color={color}
          rootElementId={uniqueId}
          describedById={helpFor(uniqueId)}
        >
          {helpMessage}
        </FormHelpMessage>
      )}
      {hasError && (
        <FormErrorMessage
          className={classes.message}
          color={color}
          rootElementId={uniqueId}
          describedById={errorFor(uniqueId)}
        >
          {errorMessage}
        </FormErrorMessage>
      )}
      {/*
        A few things here:

        1) We want to always portal our select menu results so we don't
           run into weird layout issues (rendering inside Popover or Modal)
        2) We need to trap focus inside of the menu when it is open - <FocusLock /> handles this for us
        3) We need keyboard support via <Rover />

        Reference for #1 & #2: https://github.com/reakit/reakit/issues/566
      */}
      <Portal>
        <FocusLock>
          <ReakitPopover
            aria-label={label || ariaLabel || popoverAriaLabel}
            className={classes.popover}
            {...popover}
            style={{ width }}
            as={motion.div}
            animate={popover.visible ? 'open' : 'closed'}
            variants={
              shouldReduceMotion ? popoverVariantsReduced : popoverVariants
            }
          >
            <motion.ul
              className={classes.ul}
              role="listbox"
              variants={
                shouldReduceMotion
                  ? listMotionVariantsReduced
                  : listMotionVariants
              }
            >
              {popover.visible &&
                React.Children.map(children, (child) => {
                  if (!React.isValidElement(child)) {
                    return null;
                  }

                  if (isHeadingElement(child)) {
                    return child;
                  }

                  const option: React.ReactElement<SelectOptionProps> = child;

                  return (
                    <RoverOption
                      rover={rover}
                      value={value}
                      option={option}
                      handleOptionSelect={handleOptionSelected}
                      variants={
                        shouldReduceMotion
                          ? listItemMotionVariantsReduced
                          : listItemMotionVariants
                      }
                      disabled={child?.props?.disabled}
                    />
                  );
                })}
            </motion.ul>
          </ReakitPopover>
        </FocusLock>
      </Portal>
    </div>
  );
}
Example #10
Source File: Snackbar.tsx    From chroma-react with MIT License 4 votes vote down vote up
Snackbar: React.FC<SnackbarProps> = React.forwardRef<
  HTMLDivElement,
  SnackbarProps
>(
  (
    {
      className,
      duration = 6000,
      icon: Icon,
      isOpen = false,
      allowDismiss = false,
      onClose,
      role = 'status',
      statusType = 'info',
      title,
      children,
      ...rootProps
    },
    ref
  ) => {
    const classes = useStyles({});

    const shouldReduceMotion = useReducedMotion();

    const [snackbarTimeout, setSnackbarTimeout] = React.useState<number | null>(
      duration
    );

    // Event handlers
    const onMouseEnter = () => setSnackbarTimeout(null);
    const onMouseLeave = () => setSnackbarTimeout(duration);
    const closeSnackbar = React.useCallback(() => {
      onClose && onClose();
    }, [onClose]);

    // Use a ref to close our Snackbar after the timeout
    const callbackRef = React.useRef<() => void | null>();

    React.useEffect(() => {
      if (!callbackRef.current) {
        callbackRef.current = closeSnackbar;
      }
    }, [closeSnackbar]);

    React.useEffect(() => {
      // Ignore setting up a timer for the Snackbar
      // if one is not isOpen.
      if (!isOpen) {
        return;
      }

      const tick = () => {
        if (callbackRef.current) {
          callbackRef.current();
        }
      };

      if (snackbarTimeout) {
        const id = setTimeout(tick, snackbarTimeout);
        return () => clearTimeout(id);
      }
    }, [snackbarTimeout, isOpen]);

    return (
      <AnimatePresence initial={false}>
        {isOpen ? (
          <motion.div
            ref={ref}
            className={clsx(
              classes.root,
              {
                [classes.infoModifier]: statusType === 'info',
                [classes.errorModifier]: statusType === 'error',
                [classes.warningModifier]: statusType === 'warning',
                [classes.successModifier]: statusType === 'success',
              },
              className
            )}
            aria-live={role === 'alert' ? 'assertive' : 'polite'}
            role={role}
            onMouseEnter={onMouseEnter}
            onMouseLeave={onMouseLeave}
            positionTransition
            initial={
              shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: -40 }
            }
            animate={
              shouldReduceMotion
                ? { opacity: 1 }
                : {
                    opacity: 1,
                    y: 0,
                  }
            }
            exit={
              shouldReduceMotion
                ? { opacity: 0 }
                : {
                    opacity: 0,
                    y: 60,
                    transition: { duration: 0.25, ease: 'easeIn' },
                  }
            }
            {...rootProps}
          >
            {!!Icon && <Icon role="img" aria-hidden className={classes.icon} />}
            {children ? (
              children
            ) : (
              <Text className={classes.title}>{title}</Text>
            )}
            {allowDismiss && (
              <>
                <IconButton
                  className={classes.closeButton}
                  aria-label="Close Notification"
                  size={0}
                  paddingTop={0}
                  paddingBottom={0}
                  paddingRight={0}
                  icon={X}
                  onClick={closeSnackbar}
                />
              </>
            )}
          </motion.div>
        ) : null}
      </AnimatePresence>
    );
  }
)