{"version":3,"file":"commandPalette.mjs","names":[],"sources":["../../../../package/common/commandPalette/commandPalette.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';\n\nimport Modal from '../modal';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { ariaKeyshortcuts, formatHotkey } from '../hooks/useHotkeys';\nimport { edgeIndex, nextIndex, scrollOptionIntoView } from '../utils/listNavigation';\nimport { renderIcon } from '../utils/renderIcon';\nimport { useFollowHref } from '../utils/navigation';\nimport type { MenuAction } from '../menu/menu.types';\nimport { hasAnyIcon, renderItemIcon, runMenuAction } from '../menu/menuItem';\nimport { filterCommands, groupCommands } from './filterCommands';\nimport type { CommandPaletteLabels, CommandPaletteProps } from './commandPalette.types';\nimport styles from './commandPalette.module.pcss';\n\nconst DEFAULT_LABELS: Required<CommandPaletteLabels> = {\n  label: 'Search commands',\n  placeholder: 'Type a command or search…',\n  empty: 'No results',\n  loading: 'Searching…',\n  results: (count) => (count === 1 ? '1 result' : `${count} results`),\n};\n\nfunction CommandPalette(props: CommandPaletteProps): ReactElement {\n  const {\n    items,\n    isOpen,\n    onClose,\n    onAction,\n    onQueryChange,\n    filter = true,\n    isLoading = false,\n    presentation,\n    labels,\n    className,\n    style,\n    classNames,\n    styles: slotStyles,\n    ...rest\n  } = useComponentDefaults('CommandPalette', props);\n  const text = useLabels('CommandPalette', labels, DEFAULT_LABELS);\n  const listId = useId();\n  const listRef = useRef<HTMLDivElement>(null);\n  const followHref = useFollowHref();\n\n  const [query, setQuery] = useState('');\n  // An id rather than an index, so the highlight stays on the same command while server results\n  // are replaced around it. Typing clears it instead, so Enter runs the best match.\n  const [activeId, setActiveId] = useState<string | null>(null);\n  const [wasOpen, setWasOpen] = useState(isOpen);\n  if (isOpen !== wasOpen) {\n    setWasOpen(isOpen);\n    if (isOpen) {\n      setQuery('');\n      setActiveId(null);\n    }\n  }\n\n  // Read through a ref so an inline handler does not re-fire the effect on every parent render.\n  const onQueryChangeRef = useRef(onQueryChange);\n  useEffect(() => {\n    onQueryChangeRef.current = onQueryChange;\n  });\n  useEffect(() => {\n    if (isOpen) onQueryChangeRef.current?.('');\n  }, [isOpen]);\n\n  const groups = filter ? filterCommands(groupCommands(items), query) : groupCommands(items);\n  // From all items, not the matches, so the labels do not jump sideways while the user types.\n  const hasIcons = hasAnyIcon(items);\n  const actions = groups.flatMap((group) => group.actions);\n  const isDisabled = (index: number) => !!actions[index]?.disabled;\n  const found = actions.findIndex((action) => action.id === activeId && !action.disabled);\n  const activeIndex = found >= 0 ? found : edgeIndex('first', actions.length, { isDisabled });\n  // From the command's id, not its position: an id that stayed the same while the option under it\n  // changed would not make a screen reader announce the new highlight.\n  const optionId = (action: MenuAction) => `${listId}-${encodeURIComponent(action.id)}`;\n  const activeOptionId = activeIndex === null ? undefined : optionId(actions[activeIndex]);\n\n  useEffect(() => {\n    if (!activeOptionId) return;\n    scrollOptionIntoView(listRef.current, document.getElementById(activeOptionId));\n  }, [activeOptionId]);\n\n  function run(action: MenuAction | undefined) {\n    if (!action || action.disabled) return;\n    runMenuAction(action, onAction, followHref);\n    onClose();\n  }\n\n  function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n    // Enter confirms an IME composition before it means anything here.\n    if (event.nativeEvent.isComposing) return;\n    if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n      const next = nextIndex(activeIndex, event.key === 'ArrowDown' ? 1 : -1, actions.length, { isDisabled });\n      setActiveId(next === null ? null : actions[next].id);\n    } else if (event.key === 'Enter') {\n      run(activeIndex === null ? undefined : actions[activeIndex]);\n    } else {\n      return;\n    }\n    event.preventDefault();\n  }\n\n  const hasQuery = query.trim() !== '';\n  const emptyText = isLoading ? text.loading : text.empty;\n  let index = -1;\n\n  return (\n    <Modal\n      {...rest}\n      aria-label={text.label}\n      isOpen={isOpen}\n      onClose={onClose}\n      hideHeader\n      presentation={presentation}\n      className={cx(styles.commandPalette, className, classNames?.root)}\n      style={{ ...style, ...slotStyles?.root }}\n    >\n      <div className={cx(styles.commandPaletteSearch, classNames?.search)} style={slotStyles?.search}>\n        {renderIcon(faMagnifyingGlass, { className: styles.commandPaletteSearchIcon })}\n        <input\n          type='text'\n          role='combobox'\n          aria-label={text.label}\n          aria-expanded='true'\n          aria-controls={listId}\n          aria-autocomplete='list'\n          aria-activedescendant={activeOptionId}\n          autoComplete='off'\n          spellCheck={false}\n          placeholder={text.placeholder}\n          value={query}\n          onChange={(event) => {\n            setQuery(event.target.value);\n            setActiveId(null);\n            onQueryChange?.(event.target.value);\n          }}\n          onKeyDown={handleKeyDown}\n          className={cx(styles.commandPaletteInput, classNames?.input)}\n          style={slotStyles?.input}\n        />\n      </div>\n\n      <div\n        ref={listRef}\n        id={listId}\n        role='listbox'\n        aria-label={text.label}\n        className={cx(styles.commandPaletteList, classNames?.list)}\n        style={slotStyles?.list}\n      >\n        {groups.map((group, groupIndex) => {\n          const headingId = `${listId}-heading-${groupIndex}`;\n          return (\n            <div\n              // Groups carry no id of their own; a new query re-renders the list wholesale anyway.\n              key={groupIndex}\n              role={group.heading === undefined ? 'presentation' : 'group'}\n              aria-labelledby={group.heading === undefined ? undefined : headingId}\n            >\n              {group.separated && groupIndex > 0 && (\n                <div\n                  aria-hidden\n                  className={cx(styles.commandPaletteSeparator, classNames?.separator)}\n                  style={slotStyles?.separator}\n                />\n              )}\n              {group.heading !== undefined && (\n                <div\n                  id={headingId}\n                  role='presentation'\n                  className={cx(styles.commandPaletteHeading, classNames?.heading)}\n                  style={slotStyles?.heading}\n                >\n                  {group.heading}\n                </div>\n              )}\n              {group.actions.map((action) => {\n                index += 1;\n                const own = index;\n                return (\n                  <div\n                    key={action.id}\n                    id={optionId(action)}\n                    role='option'\n                    aria-selected={own === activeIndex}\n                    aria-disabled={action.disabled || undefined}\n                    aria-keyshortcuts={ariaKeyshortcuts(action.shortcut)}\n                    className={cx(\n                      styles.commandPaletteItem,\n                      own === activeIndex && styles.commandPaletteItemActive,\n                      action.danger && styles.commandPaletteItemDanger,\n                      action.disabled && styles.commandPaletteItemDisabled,\n                      classNames?.item,\n                    )}\n                    style={slotStyles?.item}\n                    // Keeps the focus, and so the caret, in the search field.\n                    onMouseDown={(event) => event.preventDefault()}\n                    // Move, not enter: a list scrolling under a resting pointer must not steal the highlight.\n                    onMouseMove={() => !action.disabled && setActiveId(action.id)}\n                    onClick={() => run(action)}\n                  >\n                    {renderItemIcon(action.icon, hasIcons, {\n                      className: cx(styles.commandPaletteItemIcon, classNames?.itemIcon),\n                      style: slotStyles?.itemIcon,\n                    })}\n                    <span\n                      className={cx(styles.commandPaletteItemLabel, classNames?.itemLabel)}\n                      style={slotStyles?.itemLabel}\n                    >\n                      {action.label}\n                    </span>\n                    {action.shortcut != null && (\n                      <span\n                        aria-hidden\n                        className={cx(styles.commandPaletteItemShortcut, classNames?.itemShortcut)}\n                        style={slotStyles?.itemShortcut}\n                      >\n                        {formatHotkey(action.shortcut)}\n                      </span>\n                    )}\n                  </div>\n                );\n              })}\n            </div>\n          );\n        })}\n      </div>\n\n      {hasQuery && actions.length === 0 && (\n        <div className={cx(styles.commandPaletteEmpty, classNames?.empty)} style={slotStyles?.empty}>\n          {emptyText}\n        </div>\n      )}\n      <span className={styles.commandPaletteStatus} role='status' aria-live='polite'>\n        {hasQuery ? (actions.length > 0 ? text.results(actions.length) : emptyText) : ''}\n      </span>\n    </Modal>\n  );\n}\n\nexport default CommandPalette;\n"],"mappings":"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"}