{"version":3,"file":"Dropdown.cjs","names":[],"sources":["../../../src/components/Dropdown/Dropdown.tsx"],"sourcesContent":["/*\nCopyright 2024 New Vector Ltd.\n\nSPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial\nPlease see LICENSE files in the repository root for full details.\n*/\n\nimport ChevronDown from \"@vector-im/compound-design-tokens/assets/web/icons/chevron-down\";\nimport Check from \"@vector-im/compound-design-tokens/assets/web/icons/check\";\nimport Error from \"@vector-im/compound-design-tokens/assets/web/icons/error-solid\";\n\nimport React, {\n  type Dispatch,\n  forwardRef,\n  type HTMLProps,\n  memo,\n  type RefObject,\n  type SetStateAction,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type KeyboardEvent,\n  useMemo,\n} from \"react\";\n\nimport classNames from \"classnames\";\n\nimport styles from \"./Dropdown.module.css\";\nimport { useId } from \"@floating-ui/react\";\n\ntype DropdownProps = {\n  /**\n   * The CSS class name.\n   */\n  className?: string;\n  /**\n   * The controlled value of the dropdown.\n   */\n  value?: string;\n  /**\n   * The default value of the dropdown, used when uncontrolled.\n   */\n  defaultValue?: string;\n  /**\n   * The values of the dropdown.\n   * [value, text]\n   */\n  values: [string, string][];\n  /**\n   * The placeholder text.\n   */\n  placeholder: string;\n  /**\n   * The label to display at the top of the dropdown\n   */\n  label: string;\n  /**\n   * The help label to display at the bottom of the dropdown\n   */\n  helpLabel?: string;\n  /**\n   * Callback for when the value changes.\n   * @param value\n   */\n  onValueChange?: (value: string) => void;\n  /**\n   * The error message to display.\n   */\n  error?: string;\n};\n\n/**\n * The dropdown content.\n */\nexport const Dropdown = forwardRef<HTMLButtonElement, DropdownProps>(\n  function Dropdown(\n    {\n      className,\n      label,\n      placeholder,\n      helpLabel,\n      onValueChange,\n      error,\n      value: controlledValue,\n      defaultValue,\n      values,\n      ...props\n    },\n    ref,\n  ) {\n    const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);\n    const value = controlledValue ?? uncontrolledValue;\n    const text = useMemo(\n      () =>\n        value === undefined\n          ? placeholder\n          : (values.find(([v]) => v === value)?.[1] ?? placeholder),\n      [value, values, placeholder],\n    );\n\n    const setValue = useCallback(\n      (value: string) => {\n        setUncontrolledValue(value);\n        onValueChange?.(value);\n      },\n      [setUncontrolledValue, onValueChange],\n    );\n\n    const [open, setOpen, dropdownRef] = useOpen();\n    const { listRef, onComboboxKeyDown, onOptionKeyDown } = useKeyboardShortcut(\n      open,\n      setOpen,\n      setValue,\n    );\n\n    const buttonRef = useRef<HTMLButtonElement | null>(null);\n    useEffect(() => {\n      // Focus the button when the value is set\n      // Test if the value is undefined to avoid focusing on the first render\n      if (value !== undefined) buttonRef.current?.focus();\n    }, [value]);\n\n    const hasPlaceholder = text === placeholder;\n    const buttonClasses = classNames({\n      [styles.placeholder]: hasPlaceholder,\n    });\n    const borderClasses = classNames(styles.border, {\n      [styles.open]: open,\n    });\n    const contentClasses = classNames(styles.content, {\n      [styles.open]: open,\n    });\n\n    /**\n     * Ids for accessibility.\n     */\n    const labelId = useId();\n    const contentId = useId();\n\n    return (\n      <div\n        ref={dropdownRef}\n        className={classNames(className, styles.container)}\n        aria-invalid={Boolean(error)}\n      >\n        <label id={labelId}>{label}</label>\n        <button\n          className={buttonClasses}\n          role=\"combobox\"\n          aria-haspopup=\"listbox\"\n          aria-labelledby={labelId}\n          aria-controls={contentId}\n          aria-expanded={open}\n          ref={(element) => {\n            // Private ref to focus the button\n            buttonRef.current = element;\n            // Handle forwarded ref\n            if (typeof ref === \"function\") {\n              ref(element);\n            } else if (ref) {\n              ref.current = element;\n            }\n          }}\n          onClick={() => setOpen((_open) => !_open)}\n          onKeyDown={onComboboxKeyDown}\n          {...props}\n        >\n          {text}\n          <ChevronDown width=\"24\" height=\"24\" />\n        </button>\n        <div className={borderClasses} />\n        <div className={contentClasses}>\n          <ul\n            ref={listRef}\n            id={contentId}\n            role=\"listbox\"\n            className={styles.content}\n          >\n            {values.map(([v, text]) => (\n              <DropdownItem\n                key={v}\n                isDisplayed={open}\n                isSelected={value === v}\n                onClick={() => {\n                  setOpen(false);\n                  setValue(v);\n                }}\n                onKeyDown={(e) => onOptionKeyDown(e, v)}\n              >\n                {text}\n              </DropdownItem>\n            ))}\n          </ul>\n        </div>\n        {!error && helpLabel && (\n          <span className={styles.help}>{helpLabel}</span>\n        )}\n        {error && (\n          <span className={styles.error}>\n            <Error width=\"20\" height=\"20\" />\n            {error}\n          </span>\n        )}\n      </div>\n    );\n  },\n);\n\ntype DropdownItemProps = HTMLProps<HTMLLIElement> & {\n  /**\n   * Whether the dropdown item is selected.\n   */\n  isSelected: boolean;\n  /**\n   * Whether the dropdown item is displayed.\n   */\n  isDisplayed: boolean;\n  /**\n   * The text to display in the dropdown item.\n   */\n  children: string;\n};\n\n/**\n * A dropdown item component.\n */\nconst DropdownItem = memo(function DropdownItem({\n  children,\n  isSelected,\n  isDisplayed,\n  ...props\n}: DropdownItemProps) {\n  const ref = useRef<HTMLLIElement>(null);\n\n  // Focus the item if the dropdown is open and the item is already selected\n  useEffect(() => {\n    if (isSelected && isDisplayed) {\n      ref.current?.focus();\n    }\n  }, [isSelected, isDisplayed]);\n\n  return (\n    <li\n      tabIndex={0}\n      role=\"option\"\n      ref={ref}\n      aria-selected={isSelected}\n      {...props}\n    >\n      {children} {isSelected && <Check width=\"20\" height=\"20\" />}\n    </li>\n  );\n});\n\n/**\n * A hook to manage the open state of the dropdown.\n */\nfunction useOpen(): [\n  boolean,\n  Dispatch<SetStateAction<boolean>>,\n  RefObject<HTMLDivElement | null>,\n] {\n  const [open, setOpen] = useState(false);\n  const ref = useRef<HTMLDivElement | null>(null);\n\n  // If the user clicks outside the dropdown, close it\n  useEffect(() => {\n    const closeIfOutside = (e: MouseEvent) => {\n      if (ref.current && !ref.current.contains(e.target as Node)) {\n        setOpen(false);\n      }\n    };\n\n    document.addEventListener(\"click\", closeIfOutside);\n    return () => document.removeEventListener(\"click\", closeIfOutside);\n  }, [setOpen]);\n\n  return [open, setOpen, ref];\n}\n\n/**\n * A hook to manage the keyboard shortcuts of the dropdown.\n * @param open - the dropdown open state.\n * @param setOpen - the dropdown open state setter.\n * @param setValue - set the selected value and text\n */\nfunction useKeyboardShortcut(\n  open: boolean,\n  setOpen: Dispatch<SetStateAction<boolean>>,\n  setValue: (value: string) => void,\n) {\n  const listRef = useRef<HTMLUListElement>(null);\n  const onComboboxKeyDown = useCallback(\n    ({ key }: KeyboardEvent) => {\n      switch (key) {\n        // Enter and Space already managed because it's a button\n        case \"Escape\":\n          setOpen(false);\n          break;\n        case \"ArrowDown\":\n          setOpen(true);\n          // If open, focus the first element\n          if (open) {\n            (listRef.current?.firstElementChild as HTMLElement)?.focus();\n          }\n          break;\n        case \"ArrowUp\":\n          setOpen(true);\n          break;\n        case \"Home\": {\n          setOpen(true);\n          // Wait for the dropdown to be opened\n          Promise.resolve().then(() => {\n            (listRef.current?.firstElementChild as HTMLElement)?.focus();\n          });\n          break;\n        }\n        case \"End\": {\n          setOpen(true);\n          // Wait for the dropdown to be opened\n          Promise.resolve().then(() => {\n            (listRef.current?.lastElementChild as HTMLElement)?.focus();\n          });\n          break;\n        }\n      }\n    },\n    [listRef, open, setOpen],\n  );\n\n  const onOptionKeyDown = useCallback(\n    (evt: KeyboardEvent, value: string) => {\n      const { key, altKey } = evt;\n      evt.stopPropagation();\n      evt.preventDefault();\n\n      switch (key) {\n        case \"Enter\":\n        case \" \": {\n          setValue(value);\n          setOpen(false);\n          break;\n        }\n        case \"Tab\":\n        case \"Escape\":\n          setOpen(false);\n          break;\n        case \"ArrowDown\": {\n          const currentFocus = document.activeElement;\n          if (listRef.current?.contains(currentFocus) && currentFocus) {\n            (currentFocus.nextElementSibling as HTMLElement)?.focus();\n          }\n          break;\n        }\n        case \"ArrowUp\": {\n          if (altKey) {\n            setValue(value);\n            setOpen(false);\n          } else {\n            const currentFocus = document.activeElement;\n            if (listRef.current?.contains(currentFocus) && currentFocus) {\n              (currentFocus.previousElementSibling as HTMLElement)?.focus();\n            }\n          }\n          break;\n        }\n        case \"Home\": {\n          (listRef.current?.firstElementChild as HTMLElement)?.focus();\n          break;\n        }\n        case \"End\": {\n          (listRef.current?.lastElementChild as HTMLElement)?.focus();\n          break;\n        }\n      }\n    },\n    [listRef, setValue, setOpen],\n  );\n\n  return { listRef, onComboboxKeyDown, onOptionKeyDown };\n}\n"],"mappings":"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"}