UNPKG

braid-design-system

Version:
635 lines (634 loc) • 24.9 kB
"use strict"; const jsxRuntime = require("react/jsx-runtime"); const matchHighlights = require("autosuggest-highlight/match/index.js"); const parseHighlights = require("autosuggest-highlight/parse/index.js"); const dedent = require("dedent"); const react = require("react"); const reactRemoveScroll = require("react-remove-scroll"); const lib_css_typography_cjs = require("../../css/typography.cjs"); const lib_hooks_useFallbackId_cjs = require("../../hooks/useFallbackId.cjs"); const lib_translations_en_cjs = require("../../translations/en.cjs"); const lib_components_Box_Box_cjs = require("../Box/Box.cjs"); const lib_components_ButtonIcon_ButtonIcon_cjs = require("../ButtonIcon/ButtonIcon.cjs"); const lib_components_HiddenVisually_HiddenVisually_cjs = require("../HiddenVisually/HiddenVisually.cjs"); const lib_components_Strong_Strong_cjs = require("../Strong/Strong.cjs"); const lib_components_Text_Text_cjs = require("../Text/Text.cjs"); const lib_components_private_Announcement_Announcement_cjs = require("../private/Announcement/Announcement.cjs"); const lib_components_private_Field_ClearField_cjs = require("../private/Field/ClearField.cjs"); const lib_components_private_Field_Field_cjs = require("../private/Field/Field.cjs"); const lib_components_private_Popover_Popover_cjs = require("../private/Popover/Popover.cjs"); const lib_components_private_getNextIndex_cjs = require("../private/getNextIndex.cjs"); const lib_components_private_normalizeKey_cjs = require("../private/normalizeKey.cjs"); const lib_components_private_smoothScroll_cjs = require("../private/smoothScroll.cjs"); const lib_components_useResponsiveValue_useResponsiveValue_cjs = require("../useResponsiveValue/useResponsiveValue.cjs"); const lib_components_Autosuggest_createAccessibilityProps_cjs = require("./createAccessibilityProps.cjs"); const lib_components_Autosuggest_reverseMatches_cjs = require("./reverseMatches.cjs"); const lib_components_Autosuggest_Autosuggest_css_cjs = require("./Autosuggest.css.cjs"); const lib_css_typography_css_cjs = require("../../css/typography.css.cjs"); const lib_components_icons_IconClear_IconClear_cjs = require("../icons/IconClear/IconClear.cjs"); const _interopDefaultCompat = (e) => e && typeof e === "object" && "default" in e ? e : { default: e }; const matchHighlights__default = /* @__PURE__ */ _interopDefaultCompat(matchHighlights); const parseHighlights__default = /* @__PURE__ */ _interopDefaultCompat(parseHighlights); const dedent__default = /* @__PURE__ */ _interopDefaultCompat(dedent); const INPUT_FOCUS = 0; const INPUT_BLUR = 1; const INPUT_CHANGE = 2; const INPUT_ARROW_UP = 3; const INPUT_ARROW_DOWN = 4; const INPUT_ESCAPE = 5; const INPUT_ENTER = 6; const SUGGESTION_MOUSE_CLICK = 7; const SUGGESTION_MOUSE_ENTER = 8; const HAS_SUGGESTIONS_CHANGED = 9; function SuggestionItem({ suggestion, highlighted, selected, onHover, id, ...restProps }) { const { highlights = [], onClear, clearLabel } = suggestion; const label = suggestion.label ?? suggestion.text; const suggestionParts = parseHighlights__default.default( label, highlights.map(({ start, end }) => [start, end]) ); return /* @__PURE__ */ jsxRuntime.jsx( lib_components_Box_Box_cjs.Box, { component: "li", cursor: "pointer", onMouseDown: (event) => { event.preventDefault(); }, onMouseMove: onHover, onTouchStart: onHover, id, ...restProps, children: /* @__PURE__ */ jsxRuntime.jsxs( lib_components_Box_Box_cjs.Box, { component: "span", display: "flex", justifyContent: "spaceBetween", background: highlighted ? "formAccentSoft" : void 0, paddingX: "small", paddingRight: onClear ? "none" : void 0, children: [ /* @__PURE__ */ jsxRuntime.jsxs(lib_components_Box_Box_cjs.Box, { className: lib_css_typography_css_cjs.touchableText.standard, children: [ /* @__PURE__ */ jsxRuntime.jsx(lib_components_Text_Text_cjs.Text, { baseline: false, children: suggestionParts.map( ({ highlight, text }, index) => selected || highlight ? /* @__PURE__ */ jsxRuntime.jsx(lib_components_Strong_Strong_cjs.Strong, { children: text }, index) : /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: text }, index) ) }), suggestion.description ? /* @__PURE__ */ jsxRuntime.jsx(lib_components_Text_Text_cjs.Text, { size: "small", tone: "secondary", baseline: false, children: suggestion.description }) : null ] }), typeof onClear === "function" ? /* @__PURE__ */ jsxRuntime.jsx( lib_components_Box_Box_cjs.Box, { display: "flex", alignItems: "center", justifyContent: "center", width: "touchable", height: "touchable", children: /* @__PURE__ */ jsxRuntime.jsx( lib_components_ButtonIcon_ButtonIcon_cjs.ButtonIcon, { icon: /* @__PURE__ */ jsxRuntime.jsx(lib_components_icons_IconClear_IconClear_cjs.IconClear, { tone: "secondary" }), tabIndex: -1, size: "small", label: clearLabel || "Clear suggestion", onClick: (event) => { event.preventDefault(); event.stopPropagation(); onClear(valueFromSuggestion(suggestion)); } } ) } ) : null ] } ) } ); } function GroupHeading({ children }) { return /* @__PURE__ */ jsxRuntime.jsx( lib_components_Box_Box_cjs.Box, { paddingX: "small", className: [ lib_css_typography_css_cjs.touchableText.xsmall, lib_css_typography_cjs.textStyles({ size: "small", baseline: true, weight: "strong", tone: "secondary" }) ], "data-testid": process.env.NODE_ENV !== "production" ? `group-heading-${children}` : void 0, children } ); } function normaliseSuggestions(suggestions) { let index = 0; const normalisedSuggestions = []; const groupHeadingIndexes = /* @__PURE__ */ new Map(); const groupHeadingForSuggestion = /* @__PURE__ */ new Map(); for (const item of suggestions) { if ("suggestions" in item) { groupHeadingIndexes.set(index, item.label); item.suggestions.forEach((suggestion) => { groupHeadingForSuggestion.set(suggestion, item.label); }); index = normalisedSuggestions.push(...item.suggestions); } else { index = normalisedSuggestions.push(item); } } return { normalisedSuggestions, groupHeadingIndexes, groupHeadingForSuggestion }; } function valueFromSuggestion(suggestion) { return "value" in suggestion ? { text: suggestion.text, value: suggestion.value } : { text: suggestion.text }; } const noop = () => { }; const fallbackValue = { text: "" }; const fallbackSuggestions = []; function normaliseNoSuggestionMessage(noSuggestionsMessage) { return typeof noSuggestionsMessage === "string" ? { description: noSuggestionsMessage } : noSuggestionsMessage; } function highlightSuggestions(suggestion, value, variant = "matching") { const matchedHighlights = matchHighlights__default.default(suggestion, value); const matches = variant === "matching" ? matchedHighlights : lib_components_Autosuggest_reverseMatches_cjs.reverseMatches(suggestion, matchedHighlights); return matches.map(([start, end]) => ({ start, end })); } const Autosuggest = react.forwardRef(function({ id, value = fallbackValue, suggestions: suggestionsProp = fallbackSuggestions, noSuggestionsMessage: noSuggestionsMessageProp, onChange = noop, automaticSelection = false, suggestionHighlight, showMobileBackdrop = false, scrollToTopOnMobile = true, hideSuggestionsOnSelection = true, onFocus = noop, onBlur = noop, placeholder, type = "text", clearLabel, onClear, translations = lib_translations_en_cjs.autosuggest, ...restProps }, forwardedRef) { const suggestionsPropValue = typeof suggestionsProp === "function" ? suggestionsProp(value) : suggestionsProp; const suggestions = Array.isArray(suggestionsPropValue) ? suggestionsPropValue : []; const noSuggestionsMessage = normaliseNoSuggestionMessage( noSuggestionsMessageProp ); const hasItems = suggestions.length > 0 || Boolean(noSuggestionsMessage); const hasExplicitHighlights = suggestions.some( (suggestion) => "highlights" in suggestion ); if (process.env.NODE_ENV !== "production") { if (suggestionHighlight && hasExplicitHighlights) { console.warn( dedent__default.default` In Autosuggest, you are using the "suggestionHighlight" prop with suggestions that have individual highlight ranges. Your provided highlight ranges will be overridden. If you want to use your own highlight ranges, remove the "suggestionHighlight" prop. ` ); } } const resolvedId = lib_hooks_useFallbackId_cjs.useFallbackId(id); const defaultRef = react.useRef(null); const inputRef = forwardedRef || defaultRef; const fireChange = react.useCallback( (suggestion) => onChange(valueFromSuggestion(suggestion)), [onChange] ); const fieldRef = react.useRef(null); const rootRef = react.useRef(null); const menuRef = react.useRef(null); const justPressedArrowRef = react.useRef(false); const { normalisedSuggestions, groupHeadingIndexes, groupHeadingForSuggestion } = normaliseSuggestions(suggestions); const suggestionCount = normalisedSuggestions.length; const hasSuggestions = suggestionCount > 0; const reducer = (state, action) => { switch (action.type) { case INPUT_ARROW_DOWN: { if (hasSuggestions) { const nextIndex = lib_components_private_getNextIndex_cjs.getNextIndex( 1, state.highlightedIndex, suggestionCount ); return { ...state, showSuggestionsIfAvailable: true, previewValue: normalisedSuggestions[nextIndex], highlightedIndex: nextIndex }; } return state; } case INPUT_ARROW_UP: { if (hasSuggestions) { const nextIndex = lib_components_private_getNextIndex_cjs.getNextIndex( -1, state.highlightedIndex, suggestionCount ); return { ...state, showSuggestionsIfAvailable: true, previewValue: normalisedSuggestions[nextIndex], highlightedIndex: nextIndex }; } return state; } case INPUT_CHANGE: { return { ...state, showSuggestionsIfAvailable: true, inputChangedSinceFocus: true, previewValue: null, highlightedIndex: hasSuggestions && automaticSelection && value.text.length > 0 ? 0 : null }; } case INPUT_FOCUS: { return { ...state, showSuggestionsIfAvailable: true, inputChangedSinceFocus: false, isFocused: true }; } case INPUT_BLUR: { return { ...state, showSuggestionsIfAvailable: false, previewValue: null, highlightedIndex: null, isFocused: false }; } case INPUT_ESCAPE: { if (value.text) { return { ...state, showSuggestionsIfAvailable: false, previewValue: null, highlightedIndex: null }; } else if (hasItems) { return { ...state, showSuggestionsIfAvailable: !state.showSuggestionsIfAvailable, previewValue: null }; } return state; } case INPUT_ENTER: case SUGGESTION_MOUSE_CLICK: { return { ...state, showSuggestionsIfAvailable: !hideSuggestionsOnSelection, previewValue: null, highlightedIndex: null }; } case SUGGESTION_MOUSE_ENTER: { return { ...state, highlightedIndex: action.value }; } case HAS_SUGGESTIONS_CHANGED: { return automaticSelection ? { ...state, highlightedIndex: hasSuggestions && value.text.length > 0 ? 0 : null } : state; } default: { console.error(new Error(`Invalid Autosuggest action: ${action}`)); return state; } } }; const [ { showSuggestionsIfAvailable, inputChangedSinceFocus, previewValue, highlightedIndex, isFocused }, dispatch ] = react.useReducer(reducer, { showSuggestionsIfAvailable: false, inputChangedSinceFocus: false, previewValue: null, highlightedIndex: null, isFocused: false }); const isOpen = showSuggestionsIfAvailable && hasItems; const highlightedItem = typeof highlightedIndex === "number" ? document.getElementById(lib_components_Autosuggest_createAccessibilityProps_cjs.getItemId(resolvedId, highlightedIndex)) : null; highlightedItem == null ? void 0 : highlightedItem.scrollIntoView({ block: "nearest" }); react.useEffect(() => { dispatch({ type: HAS_SUGGESTIONS_CHANGED }); }, [hasSuggestions]); const isMobile = lib_components_useResponsiveValue_useResponsiveValue_cjs.useResponsiveValue()({ mobile: true, tablet: false }); react.useEffect(() => { if (menuRef.current && isOpen && !isMobile) { const { bottom: menuBottom } = menuRef.current.getBoundingClientRect(); const viewportHeight = document.documentElement.clientHeight; if (menuBottom > viewportHeight) { menuRef.current.scrollIntoView(false); } } }, [isOpen, isMobile, suggestionCount]); const inputProps = { value: previewValue ? previewValue.text : value.text, type: type === "search" ? type : "text", placeholder: !restProps.disabled ? placeholder : void 0, onChange: (e) => { const inputValue = e.target.value; dispatch({ type: INPUT_CHANGE }); fireChange({ text: inputValue }); }, onFocus: () => { if (rootRef.current && scrollToTopOnMobile && isMobile) { lib_components_private_smoothScroll_cjs.smoothScroll(rootRef.current); } dispatch({ type: INPUT_FOCUS }); onFocus(); }, onBlur: () => { if (justPressedArrowRef.current === true) { return; } if (previewValue) { fireChange(previewValue); } else if (isOpen && automaticSelection && inputChangedSinceFocus && value.text.length > 0 && suggestionCount > 0) { fireChange(normalisedSuggestions[0]); } dispatch({ type: INPUT_BLUR }); onBlur(); }, onKeyDown: (event) => { const targetKey = lib_components_private_normalizeKey_cjs.normalizeKey(event); if (/^Arrow(Up|Down$)/.test(targetKey)) { justPressedArrowRef.current = true; setTimeout(() => { justPressedArrowRef.current = false; }, 150); } switch (targetKey) { case "ArrowDown": { event.preventDefault(); dispatch({ type: INPUT_ARROW_DOWN }); return; } case "ArrowUp": { event.preventDefault(); dispatch({ type: INPUT_ARROW_UP }); return; } case "Escape": { event.preventDefault(); if (previewValue === null && value.text) { fireChange({ text: "" }); } dispatch({ type: INPUT_ESCAPE }); return; } case "Enter": { if (typeof highlightedIndex === "number") { event.preventDefault(); fireChange(normalisedSuggestions[highlightedIndex]); } dispatch({ type: INPUT_ENTER }); return; } default: { return; } } } }; const a11y = lib_components_Autosuggest_createAccessibilityProps_cjs.createAccessibilityProps({ id: resolvedId, isOpen, highlightedIndex }); const clearable = Boolean( typeof onClear !== "undefined" && !restProps.disabled && typeof value !== "undefined" && value.text.length > 0 ); const announcements = []; const hasAutomaticSelection = automaticSelection && previewValue === null && highlightedIndex === 0; if (isFocused && isOpen && (highlightedIndex === null || hasAutomaticSelection)) { if (hasSuggestions) { announcements.push( translations.suggestionsAvailableAnnouncement(suggestionCount) ); if (hasAutomaticSelection) { announcements.push( translations.suggestionAutoSelectedAnnouncement( normalisedSuggestions[0].text ) ); } announcements.push(translations.suggestionInstructions); } else if (noSuggestionsMessage) { if (noSuggestionsMessage.title) { announcements.push(noSuggestionsMessage.title); } announcements.push(noSuggestionsMessage.description); } else { announcements.push(translations.noSuggestionsAvailableAnnouncement); } } return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ showMobileBackdrop ? /* @__PURE__ */ jsxRuntime.jsx( lib_components_Box_Box_cjs.Box, { position: "fixed", zIndex: "dropdownBackdrop", transition: "fast", display: ["block", "none"], pointerEvents: isOpen ? void 0 : "none", top: 0, left: 0, opacity: !isOpen ? 0 : void 0, className: [ lib_components_Autosuggest_Autosuggest_css_cjs.backdrop, isOpen ? lib_components_Autosuggest_Autosuggest_css_cjs.backdropVisible : void 0 ] } ) : null, /* @__PURE__ */ jsxRuntime.jsxs( lib_components_Box_Box_cjs.Box, { ...showMobileBackdrop && isOpen ? { position: "relative", zIndex: "dropdown" } : null, children: [ /* @__PURE__ */ jsxRuntime.jsxs(lib_components_Box_Box_cjs.Box, { ref: rootRef, children: [ /* @__PURE__ */ jsxRuntime.jsx( lib_components_private_Field_Field_cjs.Field, { ...restProps, componentName: "Autosuggest", id: resolvedId, value: value.text, prefix: void 0, secondaryIcon: onClear ? /* @__PURE__ */ jsxRuntime.jsx( lib_components_private_Field_ClearField_cjs.ClearField, { id: `${resolvedId}-clearfield`, hide: !clearable, onClear, label: clearLabel, inputRef } ) : null, children: (overlays, fieldProps, icon, secondaryIcon) => /* @__PURE__ */ jsxRuntime.jsxs(lib_components_Box_Box_cjs.Box, { width: "full", ref: fieldRef, children: [ /* @__PURE__ */ jsxRuntime.jsx( lib_components_Box_Box_cjs.Box, { component: "input", ...fieldProps, ...a11y.inputProps, ...inputProps, position: "relative", ref: inputRef } ), icon, overlays, secondaryIcon ] }) } ), /* @__PURE__ */ jsxRuntime.jsx( lib_components_private_Popover_Popover_cjs.Popover, { triggerRef: fieldRef, open: isOpen, width: "full", lockPlacement: true, offsetSpace: "xxsmall", modal: false, role: false, children: /* @__PURE__ */ jsxRuntime.jsx( reactRemoveScroll.RemoveScroll, { noRelative: true, ref: menuRef, forwardProps: true, children: /* @__PURE__ */ jsxRuntime.jsxs( lib_components_Box_Box_cjs.Box, { textAlign: "left", component: "ul", background: !hasSuggestions && noSuggestionsMessage ? { lightMode: "neutralSoft", darkMode: "neutral" } : "surface", borderRadius: "standard", boxShadow: "medium", width: "full", paddingY: "xxsmall", className: lib_components_Autosuggest_Autosuggest_css_cjs.menu, ...a11y.menuProps, children: [ !hasSuggestions && noSuggestionsMessage ? /* @__PURE__ */ jsxRuntime.jsxs( lib_components_Box_Box_cjs.Box, { component: "li", paddingX: "small", className: lib_css_typography_css_cjs.touchableText.standard, children: [ noSuggestionsMessage.title ? /* @__PURE__ */ jsxRuntime.jsx(lib_components_Text_Text_cjs.Text, { tone: "secondary", weight: "medium", baseline: false, children: noSuggestionsMessage.title }) : null, /* @__PURE__ */ jsxRuntime.jsx(lib_components_Text_Text_cjs.Text, { tone: "secondary", baseline: false, children: noSuggestionsMessage.description }) ] } ) : null, hasSuggestions ? normalisedSuggestions.map((suggestion, index) => { const { text } = suggestion; const groupHeading = groupHeadingIndexes.get(index); const highlights = suggestionHighlight ? highlightSuggestions( suggestion.text, value.text, suggestionHighlight ) : suggestion.highlights; return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [ groupHeading ? /* @__PURE__ */ jsxRuntime.jsx(GroupHeading, { children: groupHeading }) : null, /* @__PURE__ */ jsxRuntime.jsx( SuggestionItem, { suggestion: { ...suggestion, highlights }, highlighted: highlightedIndex === index, selected: value === suggestion, onClick: () => { fireChange(suggestion); dispatch({ type: SUGGESTION_MOUSE_CLICK }); }, onHover: () => { dispatch({ type: SUGGESTION_MOUSE_ENTER, value: index }); }, ...a11y.getItemProps({ index, label: suggestion.label ?? suggestion.text, description: suggestion.description, groupHeading: groupHeadingForSuggestion.get(suggestion) }) } ) ] }, index + text); }) : null ] } ) } ) } ) ] }), /* @__PURE__ */ jsxRuntime.jsx(lib_components_HiddenVisually_HiddenVisually_cjs.HiddenVisually, { ...a11y.assistiveDescriptionProps, children: translations.assistiveDescription }), /* @__PURE__ */ jsxRuntime.jsx(lib_components_private_Announcement_Announcement_cjs.Announcement, { children: announcements.join(". ") }) ] } ) ] }); }); Autosuggest.displayName = "Autosuggest"; exports.Autosuggest = Autosuggest; exports.highlightSuggestions = highlightSuggestions;