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 scrollIntoView from "smooth-scroll-into-view-if-needed"; import styled, { css } from "styled-components"; import Button, { Inner } from "~/components/Button"; import HelpText from "~/components/HelpText"; import useMenuHeight from "~/hooks/useMenuHeight"; import { Position, Background, Backdrop, Placement } from "./ContextMenu"; import { MenuAnchorCSS } from "./ContextMenu/MenuItem"; import { LabelText } from "./Input"; export type Option = { label: string; value: string; }; export type Props = { value?: string; label?: string; nude?: boolean; ariaLabel: string; short?: boolean; disabled?: boolean; className?: string; labelHidden?: boolean; icon?: React.ReactNode; options: Option[]; note?: React.ReactNode; onChange: (value: string | null) => void; }; const getOptionFromValue = ( options: Option[], value: string | undefined | null ) => { return options.find((option) => option.value === value); }; const InputSelect = (props: Props) => { const { value, label, className, labelHidden, options, short, ariaLabel, onChange, disabled, note, icon, } = props; const select = useSelectState({ gutter: 0, modal: true, selectedValue: value, animated: 200, }); const popOver = useSelectPopover({ ...select, hideOnClickOutside: true, preventBodyScroll: true, disabled, }); const previousValue = React.useRef(value); const contentRef = React.useRef(null); const selectedRef = React.useRef(null); const buttonRef = React.useRef(null); const [offset, setOffset] = React.useState(0); const minWidth = buttonRef.current?.offsetWidth || 0; const maxHeight = useMenuHeight( select.visible, select.unstable_disclosureRef ); React.useEffect(() => { if (previousValue.current === select.selectedValue) return; previousValue.current = select.selectedValue; async function load() { await onChange(select.selectedValue); } load(); }, [onChange, select.selectedValue]); const wrappedLabel = {label}; const selectedValueIndex = options.findIndex( (option) => option.value === select.selectedValue ); // Ensure selected option is visible when opening the input React.useEffect(() => { if (!select.animating && selectedRef.current) { scrollIntoView(selectedRef.current, { scrollMode: "if-needed", behavior: "auto", block: "start", }); } }, [select.animating]); React.useLayoutEffect(() => { if (select.visible) { const offset = Math.round( (selectedRef.current?.getBoundingClientRect().top || 0) - (contentRef.current?.getBoundingClientRect().top || 0) ); setOffset(offset); } }, [select.visible]); return ( <> {label && (labelHidden ? ( {wrappedLabel} ) : ( wrappedLabel ))} {( props: React.HTMLAttributes & { placement: Placement; } ) => { if (!props.style) { props.style = {}; } const topAnchor = props.style.top === "0"; const rightAnchor = props.placement === "bottom-end"; // offset top of select to place selected item under the cursor if (selectedValueIndex !== -1) { props.style.top = `-${offset + 32}px`; } return ( {select.visible || select.animating ? options.map((option) => ( {select.selectedValue !== undefined && ( <> {select.selectedValue === option.value ? ( ) : ( )}   )} {option.label} )) : null} ); }} {note && {note}} {(select.visible || select.animating) && } ); }; const Placeholder = styled.span` color: ${(props) => props.theme.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%; ${(props) => props.nude && css` border-color: transparent; box-shadow: none; `} ${Inner} { line-height: 28px; padding-left: 16px; padding-right: 8px; } svg { justify-self: flex-end; margin-left: auto; } `; export const StyledSelectOption = styled(SelectOption)<{ $animating: boolean }>` ${MenuAnchorCSS} ${(props) => props.$animating && css` pointer-events: none; `} `; const Wrapper = styled.label<{ short?: boolean }>` display: block; max-width: ${(props) => (props.short ? "350px" : "100%")}; `; const Positioner = styled(Position)` &.focus-visible { ${StyledSelectOption} { &[aria-selected="true"] { color: ${(props) => props.theme.white}; background: ${(props) => props.theme.primary}; box-shadow: none; cursor: pointer; svg { fill: ${(props) => props.theme.white}; } } } } `; export default InputSelect;