import { Select, SelectOption, useSelectState, useSelectPopover, SelectPopover, } from "@renderlesskit/react"; import { CheckmarkIcon } from "outline-icons"; import * as React from "react"; import { VisuallyHidden } from "reakit/VisuallyHidden"; import styled, { css } from "styled-components"; import { s } from "@shared/styles"; import Button, { Inner } from "~/components/Button"; import Text from "~/components/Text"; import useMenuHeight from "~/hooks/useMenuHeight"; import useMobile from "~/hooks/useMobile"; import { fadeAndScaleIn } from "~/styles/animations"; import { Position, Background as ContextMenuBackground, Backdrop, Placement, } from "./ContextMenu"; import { MenuAnchorCSS } from "./ContextMenu/MenuItem"; import { LabelText } from "./Input"; export type Option = { label: string | JSX.Element; value: string; }; export type Props = { id?: string; name?: string; value?: string | null; label?: string; nude?: boolean; ariaLabel: string; short?: boolean; disabled?: boolean; className?: string; labelHidden?: boolean; icon?: React.ReactNode; options: Option[]; /** @deprecated Removing soon, do not use. */ note?: React.ReactNode; onChange?: (value: string | null) => void; }; interface InnerProps extends React.HTMLAttributes { placement: Placement; } const getOptionFromValue = (options: Option[], value: string | null) => options.find((option) => option.value === value); const InputSelect = (props: Props) => { const { value = null, label, className, labelHidden, options, short, ariaLabel, onChange, disabled, note, icon, ...rest } = props; const select = useSelectState({ gutter: 0, modal: true, selectedValue: value, }); const popOver = useSelectPopover({ ...select, hideOnClickOutside: true, preventBodyScroll: true, disabled, }); const isMobile = useMobile(); const previousValue = React.useRef(value); const selectedRef = React.useRef(null); const buttonRef = React.useRef(null); const contentRef = React.useRef(null); const minWidth = buttonRef.current?.offsetWidth || 0; const margin = 8; const menuMaxHeight = useMenuHeight({ visible: select.visible, elementRef: select.unstable_disclosureRef, margin, }); const maxHeight = Math.min( menuMaxHeight ?? 0, window.innerHeight - (buttonRef.current?.getBoundingClientRect().bottom ?? 0) - margin ); const wrappedLabel = {label}; const selectedValueIndex = options.findIndex( (option) => option.value === select.selectedValue ); React.useEffect(() => { previousValue.current = value; select.setSelectedValue(value); }, [value]); React.useEffect(() => { if (previousValue.current === select.selectedValue) { return; } previousValue.current = select.selectedValue; onChange?.(select.selectedValue); }, [onChange, select.selectedValue]); React.useLayoutEffect(() => { if (select.visible) { requestAnimationFrame(() => { if (contentRef.current) { contentRef.current.scrollTop = selectedValueIndex * 32; } }); } }, [select.visible, selectedValueIndex]); return ( <> {label && (labelHidden ? ( {wrappedLabel} ) : ( wrappedLabel ))} {(props: InnerProps) => { const topAnchor = props.style?.top === "0"; const rightAnchor = props.placement === "bottom-end"; return ( {select.visible ? options.map((option) => { const isSelected = select.selectedValue === option.value; const Icon = isSelected ? CheckmarkIcon : Spacer; return (   {option.label} ); }) : null} ); }} {note && ( {note} )} {select.visible && isMobile && } ); }; const Background = styled(ContextMenuBackground)` animation: ${fadeAndScaleIn} 200ms ease; `; const Placeholder = styled.span` color: ${s("placeholder")}; `; const Spacer = styled.div` width: 24px; height: 24px; flex-shrink: 0; `; const StyledButton = styled(Button)<{ nude?: boolean }>` font-weight: normal; text-transform: none; margin-bottom: 16px; display: block; width: 100%; cursor: default; &:hover:not(:disabled) { background: ${s("buttonNeutralBackground")}; } ${(props) => props.nude && css` border-color: transparent; box-shadow: none; `} ${Inner} { line-height: 28px; padding-left: 12px; padding-right: 4px; } svg { justify-self: flex-end; margin-left: auto; } `; export const StyledSelectOption = styled(SelectOption)` ${MenuAnchorCSS} /* overriding the styles from MenuAnchorCSS because we use   here */ svg:not(:last-child) { margin-right: 0px; } `; const Wrapper = styled.label<{ short?: boolean }>` display: block; max-width: ${(props) => (props.short ? "350px" : "100%")}; `; export const Positioner = styled(Position)` &.focus-visible { ${StyledSelectOption} { &[aria-selected="true"] { color: ${(props) => props.theme.white}; background: ${s("accent")}; box-shadow: none; cursor: var(--pointer); svg { fill: ${(props) => props.theme.white}; } } } } `; export default InputSelect;