<div data-react="search-widget" data-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":""}]}"></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": ""
}
]
}
}
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;
.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));
}
}
No notes defined.