<div data-react="search-widget" data-props="{&quot;endpoint&quot;:&quot;/mocks/api/search-results.json&quot;,&quot;language&quot;:&quot;en&quot;,&quot;searchQueryKey&quot;:&quot;q&quot;,&quot;searchPageUrl&quot;:&quot;/components/preview/product-search/&quot;,&quot;placeholderText&quot;:&quot;Sök produkter&quot;,&quot;form&quot;:{&quot;method&quot;:&quot;GET&quot;},&quot;className&quot;:&quot;search-field--big search-field--with-clear-button&quot;,&quot;seeMoreText&quot;:&quot;Visa alla&quot;,&quot;enableFocusTrap&quot;:true,&quot;minQueryLength&quot;:2,&quot;closeLabel&quot;:&quot;Stäng&quot;,&quot;categoriesLabel&quot;:&quot;Search in all:&quot;,&quot;categories&quot;:[{&quot;key&quot;:&quot;all&quot;,&quot;value&quot;:&quot;All&quot;},{&quot;key&quot;:&quot;products&quot;,&quot;value&quot;:&quot;Products&quot;},{&quot;key&quot;:&quot;documents&quot;,&quot;value&quot;:&quot;Documents&quot;},{&quot;key&quot;:&quot;information&quot;,&quot;value&quot;:&quot;Information&quot;},{&quot;key&quot;:&quot;contacts&quot;,&quot;value&quot;:&quot;Contacts&quot;}],&quot;suggestionsLabel&quot;:&quot;Suggestions&quot;,&quot;suggestions&quot;:[{&quot;label&quot;:&quot;Eagle Ceiling&quot;,&quot;type&quot;:&quot;products&quot;},{&quot;label&quot;:&quot;Eagle Free&quot;,&quot;type&quot;:&quot;documents&quot;},{&quot;label&quot;:&quot;Eagle Wall&quot;,&quot;type&quot;:&quot;&quot;},{&quot;label&quot;:&quot;Eagle Single&quot;,&quot;type&quot;:&quot;information&quot;},{&quot;label&quot;:&quot;Eagle Double&quot;,&quot;type&quot;:&quot;&quot;}]}"></div>
<div data-react="search-widget" data-props="{{jsonEncode props}}"></div>
{
  "props": {
    "endpoint": "/mocks/api/search-results.json",
    "language": "en",
    "searchQueryKey": "q",
    "searchPageUrl": "/components/preview/product-search/",
    "placeholderText": "Sök produkter",
    "form": {
      "method": "GET"
    },
    "className": "search-field--big search-field--with-clear-button",
    "seeMoreText": "Visa alla",
    "enableFocusTrap": true,
    "minQueryLength": 2,
    "closeLabel": "Stäng",
    "categoriesLabel": "Search in all:",
    "categories": [
      {
        "key": "all",
        "value": "All"
      },
      {
        "key": "products",
        "value": "Products"
      },
      {
        "key": "documents",
        "value": "Documents"
      },
      {
        "key": "information",
        "value": "Information"
      },
      {
        "key": "contacts",
        "value": "Contacts"
      }
    ],
    "suggestionsLabel": "Suggestions",
    "suggestions": [
      {
        "label": "Eagle Ceiling",
        "type": "products"
      },
      {
        "label": "Eagle Free",
        "type": "documents"
      },
      {
        "label": "Eagle Wall",
        "type": ""
      },
      {
        "label": "Eagle Single",
        "type": "information"
      },
      {
        "label": "Eagle Double",
        "type": ""
      }
    ]
  }
}
  • Content:
    import React from 'react';
    import PropTypes from 'prop-types';
    import SearchField from '../search-field';
    import Spinner from '../spinner';
    import debounce from 'lodash/debounce';
    
    const highlightSearchTerm = (text, searchTerm) => {
        if (!searchTerm || searchTerm.length === 0) return text;
        const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
        return text.replace(regex, '<mark>$1</mark>');
    };
    
    const ItemComponent = (props) => (
        <a href={props.url} className="search-widget__result-link">
            <div
                className="search-widget__result-body"
                dangerouslySetInnerHTML={{ __html: props.title }}
            ></div>
        </a>
    );
    
    const SearchWidget = (props) => {
        const [searchResult, setSearchResult] = React.useState([]);
        const [seeMoreLink, setSeeMoreLink] = React.useState(null);
        const [loading, setLoading] = React.useState(false);
        const containerRef = React.useRef(null);
        const { closeLabel, enableFocusTrap, inputValue } = props;
    
        // Focus trap logic
        React.useEffect(() => {
            const container = containerRef.current;
            if (!container || !enableFocusTrap) return;
    
            const focusableSelectors = [
                'a[href]',
                'button:not([disabled])',
                'input:not([disabled])',
                '[tabindex]:not([tabindex="-1"])'
            ];
            const getFocusable = () =>
                Array.from(
                    container.querySelectorAll(focusableSelectors.join(','))
                ).filter((el) => el.offsetParent !== null);
    
            const handleKeyDown = (e) => {
                if (e.key !== 'Tab') return;
                const focusable = getFocusable();
                if (focusable.length === 0) return;
    
                const first = focusable[0];
                const last = focusable[focusable.length - 1];
                if (e.shiftKey) {
                    if (document.activeElement === first) {
                        last.focus();
                        e.preventDefault();
                    }
                } else {
                    if (document.activeElement === last) {
                        first.focus();
                        e.preventDefault();
                    }
                }
            };
    
            container.addEventListener('keydown', handleKeyDown);
            return () => container.removeEventListener('keydown', handleKeyDown);
        }, [searchResult, seeMoreLink, props.suggestions, props.enableFocusTrap]);
    
        const onSubmit = (e) => {
            e.preventDefault();
            const form = e.target;
            const query = form.elements.q.value;
    
            // Start with searchPageUrl and preserve current query params
            const url = new URL(props.searchPageUrl, window.location.origin);
    
            // Copy existing params from current URL
            const currentParams = new URLSearchParams(window.location.search);
            currentParams.forEach((value, key) => {
                url.searchParams.set(key, value);
            });
    
            // Update search query
            url.searchParams.set(props.searchQueryKey, query);
    
            // Only update activeTab if categories exist
            if (props.categories && Array.isArray(props.categories) && props.categories.length > 0) {
                const activeTab = form.elements.category ? form.elements.category.value : '';
                if (activeTab) {
                    url.searchParams.set('activeTab', activeTab);
                } else {
                    // Remove activeTab if no category is selected
                    url.searchParams.delete('activeTab');
                }
            }
    
            window.location.replace(url.toString());
        };
    
        const fetchAndSetSearchResult = debounce(async (searchQuery) => {
            const { language, form } = props;
            const method = form.method || 'GET';
            setLoading(true);
    
            const url = new URL(props.endpoint, window.location.origin);
            url.searchParams.append('query', searchQuery);
    
            const r = await fetch(url, {
                credentials: 'include',
                headers: {
                    Accept: 'application/json',
                    'Content-Type': 'application/json',
                    'Accept-Language': language || 'sv'
                },
                method: method
            });
            const data = await r.json();
            let activeTab = '';
    
            // Check is has selected category in the form
            if (containerRef.current) {
                const formElement = containerRef.current.querySelector('form');
                if (formElement && formElement.elements.category) {
                    activeTab = formElement.elements.category.value;
                }
            }
    
            // Check URL if not found in form
            if (!activeTab) {
                activeTab = new URLSearchParams(window.location.search).get('activeTab');
            }
    
            const processedResults = (data || []).map(str => {
                const item = { title: str };
    
                // Construct URL with search query and activeTab
                const resultUrl = new URL(props.searchPageUrl, window.location.origin);
                resultUrl.searchParams.set(props.searchQueryKey, item.title);
    
                if (activeTab) {
                    resultUrl.searchParams.set('activeTab', activeTab);
                }
    
                return {
                    ...item,
                    url: resultUrl.toString(),
                    title: highlightSearchTerm(item.title, searchQuery)
                };
            });
    
            setLoading(false);
            setSearchResult(() => (processedResults && processedResults.length > 0 ? processedResults : []));
            setSeeMoreLink(() => data.seeMoreLink);
        }, props.debounceTimeout);
    
        const updateSearchResult = async (e) => {
            const input = e.target.value.trim() || '';
    
            if (input.length >= props.minQueryLength) {
                fetchAndSetSearchResult(input);
            } else if (input.length === 0) {
                // Reset state when there's no search result
                setTimeout(() => {
                    setSearchResult(() => []);
                }, props.debounceTimeout);
            }
        };
    
        let suggestionsWithUrls = [];
    
        if (props.suggestions && Array.isArray(props.suggestions) && props.suggestions.length > 0) {
            suggestionsWithUrls = props.suggestions.map(function (item) {
                const type = item.type || '';
                const label = item.label;
                const query = '?activeTab=' + type + '&q=' + encodeURIComponent(label);
                return Object.assign({}, item, {
                    url: props.searchPageUrl + query
                });
            });
        }
    
        return (
            <div className="search-widget__inner" ref={containerRef}>
                <SearchField
                    items={searchResult}
                    onInputChange={updateSearchResult}
                    onSubmit={onSubmit}
                    itemStyle="search-widget__result"
                    className={props.className}
                    itemComponent={ItemComponent}
                    inputValue={inputValue}
                    inputPlaceholder={props.placeholderText}
                    preSelectedFirstItem={false}
                    onChange={(e) => window.location.replace(e.url)}
                    seeMoreLink={seeMoreLink}
                    seeMoreText={props.seeMoreText}
                    categories={{ label: props.categoriesLabel, items: props.categories }}
                    suggestionsLabel={props.suggestionsLabel}
                    suggestions={{ label: props.suggestionsLabel, items: suggestionsWithUrls }}
                >
                    {loading && <Spinner additionalClasses="search-widget__spinner" />}
                </SearchField>
                {closeLabel && (
                    <button
                        className="search-widget__close-button"
                        aria-label={closeLabel}
                        onClick={() => {
                            const container = document.querySelector('.header__search') || document;
                            container.dispatchEvent(new CustomEvent('close-header-search'));
                        }}
                    >
                        <span className="search-widget__close-button-icon" aria-hidden="true"></span>
                        {closeLabel}
                    </button>
                )}
            </div>
        );
    };
    
    SearchWidget.propTypes = {
        endpoint: PropTypes.string.isRequired,
        language: PropTypes.string.isRequired,
        searchQueryKey: PropTypes.string,
        searchPageUrl: PropTypes.string.isRequired,
        placeholderText: PropTypes.string,
        className: PropTypes.string,
        form: PropTypes.shape({
            method: PropTypes.string.isRequired
        }),
        debounceTimeout: PropTypes.number,
        minQueryLength: PropTypes.number,
        seeMoreText: PropTypes.string,
        categoriesLabel: PropTypes.string,
        categories: PropTypes.arrayOf(
            PropTypes.shape({
                value: PropTypes.string,
                label: PropTypes.string
            })
        ),
        suggestionsLabel: PropTypes.string,
        suggestions: PropTypes.arrayOf(
            PropTypes.shape({
                label: PropTypes.string,
                type: PropTypes.string
            })
        ),
        closeLabel: PropTypes.string,
        enableFocusTrap: PropTypes.bool,
        inputValue: PropTypes.string
    };
    
    SearchWidget.defaultProps = {
        searchQueryKey: 'q',
        form: {
            method: 'GET'
        },
        debounceTimeout: 200,
        minQueryLength: 2,
        seeMoreText: 'See more',
        enableFocusTrap: false
    };
    
    export default SearchWidget;
    
  • URL: /components/raw/search-widget/SearchWidget.jsx
  • Filesystem Path: src/components/search-widget/SearchWidget.jsx
  • Size: 9.3 KB
  • Content:
    .search-widget__inner {
        padding-top: 8px;
    
        box-sizing: content-box;
    
        header.header--small & {
            margin-top: size(7);
            padding: 0;
        }
    }
    
    .search-widget__result-link {
        display: flex;
        width: 100%;
        padding: 0 size(0.5);
    
        &:hover {
            text-decoration: none;
        }
    }
    
    .search-widget__result-thumbnail {
        margin-right: size(2);
        min-width: size(8);
        background-repeat: no-repeat;
        background-position: center center;
        background-size: contain;
    }
    
    .search-widget__result-body {
        color: $color-black;
    
        mark {
            display: inline-block;
            background-color: $color-green-pale;
            font-weight: $font-weight-bold;
        }
    }
    
    .search-widget__close-button {
        display: flex;
        flex-direction: column;
        align-items: center;
        min-width: 32px;
        position: absolute;
        top: size(3);
        right: size(3);
        width: size(5);
        height: size(5);
        border: 0;
        padding: 0;
        cursor: pointer;
        font-size: 10px;
    
        /* Hide close when header is small to avoid double close buttons */
        .header--small & {
            right: size(2);
        }
    
        .header__search & {
            display: none;
    
            .header--search-open & {
                display: flex;
            }
        }
    }
    
    .search-widget__close-button-icon {
        display: block;
        width: 20px;
        height: 16px;
        position: relative;
        text-align: center;
        margin-bottom: 9px;
    
        &:before,
        &:after {
            content: "";
            display: block;
            height: 2px;
            width: 100%;
            position: absolute;
            top: 50%;
            margin-top: -1px;
            background: $color-black;
        }
    
        &:before {
            transform: rotate((45deg));
        }
    
        &:after {
            transform: rotate((-45deg));
        }
    }
  • URL: /components/raw/search-widget/search-widget.scss
  • Filesystem Path: src/components/search-widget/search-widget.scss
  • Size: 1.8 KB

No notes defined.