@mrshmllw/smores-react 16.9.0 → 16.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ButtonContainer/ButtonContainer.d.ts +7 -0
- package/dist/ButtonContainer/ButtonContainer.js +19 -7
- package/dist/ButtonContainer/ButtonContainer.js.map +1 -1
- package/dist/ButtonContainer/index.d.ts +1 -1
- package/dist/ButtonContainer/index.js.map +1 -1
- package/dist/SearchInput/SearchInput.js +52 -25
- package/dist/SearchInput/SearchInput.js.map +1 -1
- package/dist/SearchInput/components/EmptyResults.d.ts +1 -0
- package/dist/SearchInput/components/EmptyResults.js +39 -6
- package/dist/SearchInput/components/EmptyResults.js.map +1 -1
- package/dist/SearchInput/components/SearchOptions.d.ts +10 -4
- package/dist/SearchInput/components/SearchOptions.js +4 -4
- package/dist/SearchInput/components/SearchOptions.js.map +1 -1
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@ type ButtonElement = ReactElement<{
|
|
|
4
4
|
'aria-describedby'?: string;
|
|
5
5
|
}>;
|
|
6
6
|
export type ButtonContainerOrientation = 'vertical' | 'horizontal';
|
|
7
|
+
export type ButtonContainerAlignment = 'center' | 'start' | 'end';
|
|
7
8
|
export type ButtonContainerSupportingMessage = {
|
|
8
9
|
/** Short plain text, e.g. reassurance copy. Required, so an icon never shows on its own */
|
|
9
10
|
text: string;
|
|
@@ -20,6 +21,12 @@ export type ButtonContainerProps = {
|
|
|
20
21
|
tertiaryButton?: ButtonElement;
|
|
21
22
|
/** Stack the buttons vertically or lay them out side by side */
|
|
22
23
|
orientation?: ButtonContainerOrientation;
|
|
24
|
+
/**
|
|
25
|
+
* Where the buttons and supporting text sit from 768px up. Use `start` or
|
|
26
|
+
* `end` to line them up with the edge of the page content. On mobile they
|
|
27
|
+
* fill the width.
|
|
28
|
+
*/
|
|
29
|
+
align?: ButtonContainerAlignment;
|
|
23
30
|
/** Optional message shown below the buttons, e.g. reassurance copy */
|
|
24
31
|
supportingMessage?: ButtonContainerSupportingMessage;
|
|
25
32
|
/**
|
|
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { Fragment, cloneElement, useEffect, useId, useRef, useState, } from 'react';
|
|
3
3
|
import styled, { css, useTheme } from 'styled-components';
|
|
4
4
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
5
|
-
export const ButtonContainer = ({ className, primaryButton, secondaryButton, tertiaryButton, orientation = 'vertical', supportingMessage, pinned = true, }) => {
|
|
5
|
+
export const ButtonContainer = ({ className, primaryButton, secondaryButton, tertiaryButton, orientation = 'vertical', align = 'center', supportingMessage, pinned = true, }) => {
|
|
6
6
|
const theme = useTheme();
|
|
7
7
|
const supportingTextId = useId();
|
|
8
8
|
const containerRef = useRef(null);
|
|
@@ -33,9 +33,14 @@ export const ButtonContainer = ({ className, primaryButton, secondaryButton, ter
|
|
|
33
33
|
const buttons = orientation === 'horizontal'
|
|
34
34
|
? [tertiaryButton, secondaryButton, describedPrimaryButton]
|
|
35
35
|
: [describedPrimaryButton, secondaryButton, tertiaryButton];
|
|
36
|
-
return (_jsxs(_Fragment, { children: [pinned && _jsx(Spacer, { "aria-hidden": "true", $height: containerHeight }), _jsxs(Container, { ref: containerRef, className: className, $pinned: pinned, children: [_jsx(ButtonGroup, { $orientation: orientation, children: buttons.map((button, index) => button && _jsx(Fragment, { children: button }, index)) }), supportingMessage && hasSupportingMessage && (_jsxs(SupportingText, { id: supportingTextId, children: [supportingMessage.icon && (_jsx(SupportingIconContainer, { children: _jsx(FontAwesomeIcon, { icon: supportingMessage.icon, color: theme.color.icon.subtle }) })), supportingMessage.text] }))] })] }));
|
|
36
|
+
return (_jsxs(_Fragment, { children: [pinned && _jsx(Spacer, { "aria-hidden": "true", $height: containerHeight }), _jsxs(Container, { ref: containerRef, className: className, $pinned: pinned, children: [_jsx(ButtonGroup, { $orientation: orientation, $align: align, children: buttons.map((button, index) => button && _jsx(Fragment, { children: button }, index)) }), supportingMessage && hasSupportingMessage && (_jsxs(SupportingText, { id: supportingTextId, $align: align, children: [supportingMessage.icon && (_jsx(SupportingIconContainer, { children: _jsx(FontAwesomeIcon, { icon: supportingMessage.icon, color: theme.color.icon.subtle }) })), supportingMessage.text] }))] })] }));
|
|
37
37
|
};
|
|
38
38
|
const desktopMediaQuery = '@media (min-width: 768px)';
|
|
39
|
+
const horizontalMargins = {
|
|
40
|
+
center: '0 auto',
|
|
41
|
+
start: '0 auto 0 0',
|
|
42
|
+
end: '0 0 0 auto',
|
|
43
|
+
};
|
|
39
44
|
const desktopVerticalButtonMaxWidth = '400px';
|
|
40
45
|
const desktopHorizontalButtonMinWidth = '160px';
|
|
41
46
|
const Spacer = styled.div(({ $height }) => css `
|
|
@@ -77,7 +82,7 @@ const Container = styled.div(({ theme, $pinned }) => css `
|
|
|
77
82
|
`}
|
|
78
83
|
`);
|
|
79
84
|
// On desktop, horizontal buttons all match the width of the longest label
|
|
80
|
-
const ButtonGroup = styled.div(({ theme, $orientation }) => css `
|
|
85
|
+
const ButtonGroup = styled.div(({ theme, $orientation, $align }) => css `
|
|
81
86
|
display: grid;
|
|
82
87
|
gap: ${theme.space[150]};
|
|
83
88
|
|
|
@@ -85,7 +90,7 @@ const ButtonGroup = styled.div(({ theme, $orientation }) => css `
|
|
|
85
90
|
css `
|
|
86
91
|
${desktopMediaQuery} {
|
|
87
92
|
grid-template-columns: minmax(0, ${desktopVerticalButtonMaxWidth});
|
|
88
|
-
justify-content:
|
|
93
|
+
justify-content: ${$align};
|
|
89
94
|
}
|
|
90
95
|
`}
|
|
91
96
|
|
|
@@ -94,12 +99,12 @@ const ButtonGroup = styled.div(({ theme, $orientation }) => css `
|
|
|
94
99
|
grid-auto-flow: column;
|
|
95
100
|
grid-auto-columns: minmax(0, 1fr);
|
|
96
101
|
|
|
97
|
-
// On desktop the buttons share the width of the longest label
|
|
102
|
+
// On desktop the buttons share the width of the longest label
|
|
98
103
|
${desktopMediaQuery} {
|
|
99
104
|
grid-auto-columns: minmax(${desktopHorizontalButtonMinWidth}, 1fr);
|
|
100
105
|
width: fit-content;
|
|
101
106
|
max-width: 100%;
|
|
102
|
-
margin:
|
|
107
|
+
margin: ${horizontalMargins[$align]};
|
|
103
108
|
}
|
|
104
109
|
`}
|
|
105
110
|
`);
|
|
@@ -120,12 +125,19 @@ const SupportingIconContainer = styled.span(({ theme }) => css `
|
|
|
120
125
|
overflow: visible;
|
|
121
126
|
}
|
|
122
127
|
`);
|
|
123
|
-
const SupportingText = styled.p(({ theme }) => css `
|
|
128
|
+
const SupportingText = styled.p(({ theme, $align }) => css `
|
|
124
129
|
margin: 0;
|
|
125
130
|
font-size: ${theme.font.body[200].fontSize};
|
|
126
131
|
font-weight: ${theme.font.body[200].fontWeight};
|
|
127
132
|
line-height: ${theme.font.body[200].lineHeight};
|
|
128
133
|
color: ${theme.color.text.subtle};
|
|
129
134
|
text-align: center;
|
|
135
|
+
|
|
136
|
+
${$align !== 'center' &&
|
|
137
|
+
css `
|
|
138
|
+
${desktopMediaQuery} {
|
|
139
|
+
text-align: ${$align};
|
|
140
|
+
}
|
|
141
|
+
`}
|
|
130
142
|
`);
|
|
131
143
|
//# sourceMappingURL=ButtonContainer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ButtonContainer.js","sourceRoot":"","sources":["../../src/ButtonContainer/ButtonContainer.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n FC,\n Fragment,\n ReactElement,\n cloneElement,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react'\nimport styled, { css, useTheme } from 'styled-components'\nimport { IconDefinition } from '@fortawesome/fontawesome-svg-core'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\n\ntype ButtonElement = ReactElement<{ 'aria-describedby'?: string }>\n\nexport type ButtonContainerOrientation = 'vertical' | 'horizontal'\n\nexport type ButtonContainerSupportingMessage = {\n /** Short plain text, e.g. reassurance copy. Required, so an icon never shows on its own */\n text: string\n /** Optional decorative icon shown before the text */\n icon?: IconDefinition\n}\n\nexport type ButtonContainerProps = {\n className?: string\n /** Main action. Shown first when vertical, last (right) when horizontal */\n primaryButton: ButtonElement\n /** Optional supporting action */\n secondaryButton?: ButtonElement\n /** Optional low-emphasis action. Shown last when vertical, first (left) when horizontal */\n tertiaryButton?: ButtonElement\n /** Stack the buttons vertically or lay them out side by side */\n orientation?: ButtonContainerOrientation\n /** Optional message shown below the buttons, e.g. reassurance copy */\n supportingMessage?: ButtonContainerSupportingMessage\n /**\n * Pin the container to the bottom of the viewport on mobile.\n * On larger screens it always sits in the page flow.\n */\n pinned?: boolean\n}\n\nexport const ButtonContainer: FC<ButtonContainerProps> = ({\n className,\n primaryButton,\n secondaryButton,\n tertiaryButton,\n orientation = 'vertical',\n supportingMessage,\n pinned = true,\n}) => {\n const theme = useTheme()\n const supportingTextId = useId()\n const containerRef = useRef<HTMLDivElement>(null)\n const [containerHeight, setContainerHeight] = useState(0)\n\n // Reserve the pinned container's height in the page flow so it never covers content\n useEffect(() => {\n const element = containerRef.current\n if (!pinned || !element || typeof ResizeObserver === 'undefined') return\n\n const observer = new ResizeObserver(() => {\n setContainerHeight(element.getBoundingClientRect().height)\n })\n observer.observe(element)\n\n return () => observer.disconnect()\n }, [pinned])\n\n const hasSupportingMessage = Boolean(supportingMessage?.text?.trim())\n\n const describedPrimaryButton = hasSupportingMessage\n ? cloneElement(primaryButton, {\n 'aria-describedby': [\n primaryButton.props['aria-describedby'],\n supportingTextId,\n ]\n .filter(Boolean)\n .join(' '),\n })\n : primaryButton\n\n // DOM order follows the visual order so keyboard focus moves the way the buttons read\n const buttons =\n orientation === 'horizontal'\n ? [tertiaryButton, secondaryButton, describedPrimaryButton]\n : [describedPrimaryButton, secondaryButton, tertiaryButton]\n\n return (\n <>\n {pinned && <Spacer aria-hidden=\"true\" $height={containerHeight} />}\n <Container ref={containerRef} className={className} $pinned={pinned}>\n <ButtonGroup $orientation={orientation}>\n {buttons.map(\n (button, index) =>\n button && <Fragment key={index}>{button}</Fragment>,\n )}\n </ButtonGroup>\n {supportingMessage && hasSupportingMessage && (\n <SupportingText id={supportingTextId}>\n {supportingMessage.icon && (\n <SupportingIconContainer>\n <FontAwesomeIcon\n icon={supportingMessage.icon}\n color={theme.color.icon.subtle}\n />\n </SupportingIconContainer>\n )}\n {supportingMessage.text}\n </SupportingText>\n )}\n </Container>\n </>\n )\n}\n\nconst desktopMediaQuery = '@media (min-width: 768px)'\nconst desktopVerticalButtonMaxWidth = '400px'\nconst desktopHorizontalButtonMinWidth = '160px'\n\nconst Spacer = styled.div<{ $height: number }>(\n ({ $height }) => css`\n height: ${$height}px;\n\n ${desktopMediaQuery} {\n display: none;\n }\n `,\n)\n\nconst Container = styled.div<{ $pinned: boolean }>(\n ({ theme, $pinned }) => css`\n display: flex;\n flex-direction: column;\n gap: ${theme.space[150]};\n width: 100%;\n // The library ships no CSS reset, so keep the pinned padding inside the viewport width\n box-sizing: border-box;\n\n ${\n $pinned &&\n css`\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: ${theme.elevation.floating};\n padding: ${theme.space[200]};\n padding-bottom: calc(\n ${theme.space[200]} + env(safe-area-inset-bottom, 0px)\n );\n background-color: ${theme.color.surface.base[200]};\n border-radius: ${theme.radius[200]} ${theme.radius[200]} 0 0;\n\n ${desktopMediaQuery} {\n position: static;\n z-index: auto;\n padding: 0;\n background-color: transparent;\n border-radius: 0;\n }\n `\n }\n `,\n)\n\n// On desktop, horizontal buttons all match the width of the longest label\nconst ButtonGroup = styled.div<{ $orientation: ButtonContainerOrientation }>(\n ({ theme, $orientation }) => css`\n display: grid;\n gap: ${theme.space[150]};\n\n ${\n $orientation === 'vertical' &&\n css`\n ${desktopMediaQuery} {\n grid-template-columns: minmax(0, ${desktopVerticalButtonMaxWidth});\n justify-content: center;\n }\n `\n }\n\n ${\n $orientation === 'horizontal' &&\n css`\n grid-auto-flow: column;\n grid-auto-columns: minmax(0, 1fr);\n\n // On desktop the buttons share the width of the longest label, centred\n ${desktopMediaQuery} {\n grid-auto-columns: minmax(${desktopHorizontalButtonMinWidth}, 1fr);\n width: fit-content;\n max-width: 100%;\n margin: 0 auto;\n }\n `\n }\n `,\n)\n\n// Figma icons use Font Awesome's square \"full\" bounding box (640 units),\n// in which the glyph's 512 unit em height takes up 80% of the box\nconst SupportingIconContainer = styled.span(\n ({ theme }) => css`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n margin-right: ${theme.space['050']};\n vertical-align: top;\n\n svg {\n width: 100%;\n height: 80%;\n overflow: visible;\n }\n `,\n)\n\nconst SupportingText = styled.p(\n ({ theme }) => css`\n margin: 0;\n font-size: ${theme.font.body[200].fontSize};\n font-weight: ${theme.font.body[200].fontWeight};\n line-height: ${theme.font.body[200].lineHeight};\n color: ${theme.color.text.subtle};\n text-align: center;\n `,\n)\n"]}
|
|
1
|
+
{"version":3,"file":"ButtonContainer.js","sourceRoot":"","sources":["../../src/ButtonContainer/ButtonContainer.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n FC,\n Fragment,\n ReactElement,\n cloneElement,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react'\nimport styled, { css, useTheme } from 'styled-components'\nimport { IconDefinition } from '@fortawesome/fontawesome-svg-core'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\n\ntype ButtonElement = ReactElement<{ 'aria-describedby'?: string }>\n\nexport type ButtonContainerOrientation = 'vertical' | 'horizontal'\n\nexport type ButtonContainerAlignment = 'center' | 'start' | 'end'\n\nexport type ButtonContainerSupportingMessage = {\n /** Short plain text, e.g. reassurance copy. Required, so an icon never shows on its own */\n text: string\n /** Optional decorative icon shown before the text */\n icon?: IconDefinition\n}\n\nexport type ButtonContainerProps = {\n className?: string\n /** Main action. Shown first when vertical, last (right) when horizontal */\n primaryButton: ButtonElement\n /** Optional supporting action */\n secondaryButton?: ButtonElement\n /** Optional low-emphasis action. Shown last when vertical, first (left) when horizontal */\n tertiaryButton?: ButtonElement\n /** Stack the buttons vertically or lay them out side by side */\n orientation?: ButtonContainerOrientation\n /**\n * Where the buttons and supporting text sit from 768px up. Use `start` or\n * `end` to line them up with the edge of the page content. On mobile they\n * fill the width.\n */\n align?: ButtonContainerAlignment\n /** Optional message shown below the buttons, e.g. reassurance copy */\n supportingMessage?: ButtonContainerSupportingMessage\n /**\n * Pin the container to the bottom of the viewport on mobile.\n * On larger screens it always sits in the page flow.\n */\n pinned?: boolean\n}\n\nexport const ButtonContainer: FC<ButtonContainerProps> = ({\n className,\n primaryButton,\n secondaryButton,\n tertiaryButton,\n orientation = 'vertical',\n align = 'center',\n supportingMessage,\n pinned = true,\n}) => {\n const theme = useTheme()\n const supportingTextId = useId()\n const containerRef = useRef<HTMLDivElement>(null)\n const [containerHeight, setContainerHeight] = useState(0)\n\n // Reserve the pinned container's height in the page flow so it never covers content\n useEffect(() => {\n const element = containerRef.current\n if (!pinned || !element || typeof ResizeObserver === 'undefined') return\n\n const observer = new ResizeObserver(() => {\n setContainerHeight(element.getBoundingClientRect().height)\n })\n observer.observe(element)\n\n return () => observer.disconnect()\n }, [pinned])\n\n const hasSupportingMessage = Boolean(supportingMessage?.text?.trim())\n\n const describedPrimaryButton = hasSupportingMessage\n ? cloneElement(primaryButton, {\n 'aria-describedby': [\n primaryButton.props['aria-describedby'],\n supportingTextId,\n ]\n .filter(Boolean)\n .join(' '),\n })\n : primaryButton\n\n // DOM order follows the visual order so keyboard focus moves the way the buttons read\n const buttons =\n orientation === 'horizontal'\n ? [tertiaryButton, secondaryButton, describedPrimaryButton]\n : [describedPrimaryButton, secondaryButton, tertiaryButton]\n\n return (\n <>\n {pinned && <Spacer aria-hidden=\"true\" $height={containerHeight} />}\n <Container ref={containerRef} className={className} $pinned={pinned}>\n <ButtonGroup $orientation={orientation} $align={align}>\n {buttons.map(\n (button, index) =>\n button && <Fragment key={index}>{button}</Fragment>,\n )}\n </ButtonGroup>\n {supportingMessage && hasSupportingMessage && (\n <SupportingText id={supportingTextId} $align={align}>\n {supportingMessage.icon && (\n <SupportingIconContainer>\n <FontAwesomeIcon\n icon={supportingMessage.icon}\n color={theme.color.icon.subtle}\n />\n </SupportingIconContainer>\n )}\n {supportingMessage.text}\n </SupportingText>\n )}\n </Container>\n </>\n )\n}\n\nconst desktopMediaQuery = '@media (min-width: 768px)'\n\nconst horizontalMargins: Record<ButtonContainerAlignment, string> = {\n center: '0 auto',\n start: '0 auto 0 0',\n end: '0 0 0 auto',\n}\nconst desktopVerticalButtonMaxWidth = '400px'\nconst desktopHorizontalButtonMinWidth = '160px'\n\nconst Spacer = styled.div<{ $height: number }>(\n ({ $height }) => css`\n height: ${$height}px;\n\n ${desktopMediaQuery} {\n display: none;\n }\n `,\n)\n\nconst Container = styled.div<{ $pinned: boolean }>(\n ({ theme, $pinned }) => css`\n display: flex;\n flex-direction: column;\n gap: ${theme.space[150]};\n width: 100%;\n // The library ships no CSS reset, so keep the pinned padding inside the viewport width\n box-sizing: border-box;\n\n ${\n $pinned &&\n css`\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: ${theme.elevation.floating};\n padding: ${theme.space[200]};\n padding-bottom: calc(\n ${theme.space[200]} + env(safe-area-inset-bottom, 0px)\n );\n background-color: ${theme.color.surface.base[200]};\n border-radius: ${theme.radius[200]} ${theme.radius[200]} 0 0;\n\n ${desktopMediaQuery} {\n position: static;\n z-index: auto;\n padding: 0;\n background-color: transparent;\n border-radius: 0;\n }\n `\n }\n `,\n)\n\n// On desktop, horizontal buttons all match the width of the longest label\nconst ButtonGroup = styled.div<{\n $orientation: ButtonContainerOrientation\n $align: ButtonContainerAlignment\n}>(\n ({ theme, $orientation, $align }) => css`\n display: grid;\n gap: ${theme.space[150]};\n\n ${\n $orientation === 'vertical' &&\n css`\n ${desktopMediaQuery} {\n grid-template-columns: minmax(0, ${desktopVerticalButtonMaxWidth});\n justify-content: ${$align};\n }\n `\n }\n\n ${\n $orientation === 'horizontal' &&\n css`\n grid-auto-flow: column;\n grid-auto-columns: minmax(0, 1fr);\n\n // On desktop the buttons share the width of the longest label\n ${desktopMediaQuery} {\n grid-auto-columns: minmax(${desktopHorizontalButtonMinWidth}, 1fr);\n width: fit-content;\n max-width: 100%;\n margin: ${horizontalMargins[$align]};\n }\n `\n }\n `,\n)\n\n// Figma icons use Font Awesome's square \"full\" bounding box (640 units),\n// in which the glyph's 512 unit em height takes up 80% of the box\nconst SupportingIconContainer = styled.span(\n ({ theme }) => css`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n margin-right: ${theme.space['050']};\n vertical-align: top;\n\n svg {\n width: 100%;\n height: 80%;\n overflow: visible;\n }\n `,\n)\n\nconst SupportingText = styled.p<{ $align: ButtonContainerAlignment }>(\n ({ theme, $align }) => css`\n margin: 0;\n font-size: ${theme.font.body[200].fontSize};\n font-weight: ${theme.font.body[200].fontWeight};\n line-height: ${theme.font.body[200].lineHeight};\n color: ${theme.color.text.subtle};\n text-align: center;\n\n ${\n $align !== 'center' &&\n css`\n ${desktopMediaQuery} {\n text-align: ${$align};\n }\n `\n }\n `,\n)\n"]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { ButtonContainer } from './ButtonContainer';
|
|
2
|
-
export type { ButtonContainerOrientation, ButtonContainerProps, ButtonContainerSupportingMessage, } from './ButtonContainer';
|
|
2
|
+
export type { ButtonContainerAlignment, ButtonContainerOrientation, ButtonContainerProps, ButtonContainerSupportingMessage, } from './ButtonContainer';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ButtonContainer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA","sourcesContent":["export { ButtonContainer } from './ButtonContainer'\nexport type {\n ButtonContainerOrientation,\n ButtonContainerProps,\n ButtonContainerSupportingMessage,\n} from './ButtonContainer'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ButtonContainer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA","sourcesContent":["export { ButtonContainer } from './ButtonContainer'\nexport type {\n ButtonContainerAlignment,\n ButtonContainerOrientation,\n ButtonContainerProps,\n ButtonContainerSupportingMessage,\n} from './ButtonContainer'\n"]}
|
|
@@ -9,6 +9,7 @@ import { useOnClickOutside } from '../hooks';
|
|
|
9
9
|
import { useUniqueId } from '../utils/id';
|
|
10
10
|
import { useControllableState } from '../utils/useControlledState';
|
|
11
11
|
import { SearchOptions } from './components/SearchOptions';
|
|
12
|
+
import { visuallyHidden } from '../utils/visuallyHidden';
|
|
12
13
|
import { IconContainer } from '../sharedStyles/shared.styles';
|
|
13
14
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
14
15
|
import { faXmark, faChevronDown, faSearch, } from '@awesome.me/kit-46ca99185c/icons/classic/regular';
|
|
@@ -40,20 +41,22 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
|
|
|
40
41
|
});
|
|
41
42
|
const [searchQuery, setSearchQuery] = useState(null);
|
|
42
43
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
44
|
+
const [emptyMessage, setEmptyMessage] = useState('');
|
|
43
45
|
const selectedValueLabel = searchList.find((option) => option.label === selectedValue || option.value === selectedValue)?.label;
|
|
44
46
|
const handleBlur = () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
setSearchQuery(null);
|
|
48
|
+
};
|
|
49
|
+
// Every way of closing the list goes through here, so a reopened list
|
|
50
|
+
// never starts with a stale highlighted option.
|
|
51
|
+
const closeOptions = () => {
|
|
52
|
+
setShowOptions(false);
|
|
53
|
+
setHighlightedIndex(-1);
|
|
51
54
|
};
|
|
52
55
|
useOnClickOutside({
|
|
53
56
|
ref: wrapperRef,
|
|
54
57
|
callback: () => {
|
|
55
58
|
handleBlur();
|
|
56
|
-
|
|
59
|
+
closeOptions();
|
|
57
60
|
},
|
|
58
61
|
});
|
|
59
62
|
// oxlint-disable-next-line react/react-compiler
|
|
@@ -90,7 +93,7 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
|
|
|
90
93
|
setHighlightedIndex(-1);
|
|
91
94
|
if (query === null) {
|
|
92
95
|
setSelectedValue(null);
|
|
93
|
-
|
|
96
|
+
closeOptions();
|
|
94
97
|
}
|
|
95
98
|
else {
|
|
96
99
|
setShowOptions(true);
|
|
@@ -102,10 +105,6 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
|
|
|
102
105
|
updateSearchQuery(searchQuery);
|
|
103
106
|
setShowOptions(true);
|
|
104
107
|
}
|
|
105
|
-
else if (selectedValue !== null && searchQuery === null) {
|
|
106
|
-
setSearchQuery(selectedValueLabel || null);
|
|
107
|
-
setShowOptions(true);
|
|
108
|
-
}
|
|
109
108
|
};
|
|
110
109
|
const handleInputChange = (event) => {
|
|
111
110
|
const nextValue = event.currentTarget.value;
|
|
@@ -122,35 +121,63 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
|
|
|
122
121
|
onFound('');
|
|
123
122
|
};
|
|
124
123
|
const handleCaretClick = () => {
|
|
125
|
-
|
|
124
|
+
if (showOptions) {
|
|
125
|
+
closeOptions();
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
setShowOptions(true);
|
|
129
|
+
}
|
|
126
130
|
};
|
|
131
|
+
const listboxId = `${id}-listbox`;
|
|
132
|
+
const getOptionId = (index) => `${id}-option-${index}`;
|
|
133
|
+
const hasResults = showOptions && filteredList.length > 0;
|
|
134
|
+
const hasHighlight = hasResults &&
|
|
135
|
+
highlightedIndex >= 0 &&
|
|
136
|
+
highlightedIndex < filteredList.length;
|
|
137
|
+
const noResults = showOptions && filteredList.length === 0;
|
|
127
138
|
const handleKeyDown = (event) => {
|
|
128
|
-
if (event.key === '
|
|
139
|
+
if (event.key === 'Escape') {
|
|
140
|
+
if (!showOptions)
|
|
141
|
+
return;
|
|
129
142
|
event.preventDefault();
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
closeOptions();
|
|
145
|
+
}
|
|
146
|
+
else if (event.key === 'Enter') {
|
|
147
|
+
if (hasHighlight) {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
handleSelect(filteredList[highlightedIndex]);
|
|
150
|
+
}
|
|
132
151
|
}
|
|
133
152
|
else if (event.key === 'ArrowDown') {
|
|
134
153
|
event.preventDefault();
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
if (!showOptions) {
|
|
155
|
+
setShowOptions(true);
|
|
156
|
+
}
|
|
157
|
+
else if (filteredList.length > 0) {
|
|
158
|
+
setHighlightedIndex((highlightedIndex + 1) % filteredList.length);
|
|
159
|
+
}
|
|
137
160
|
}
|
|
138
161
|
else if (event.key === 'ArrowUp') {
|
|
139
162
|
event.preventDefault();
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
if (showOptions && filteredList.length > 0) {
|
|
164
|
+
setHighlightedIndex((highlightedIndex - 1 + filteredList.length) % filteredList.length);
|
|
165
|
+
}
|
|
142
166
|
}
|
|
143
167
|
};
|
|
144
168
|
const showClearSearchButton = !!clearSearch && (!!value || !!selectedValue || !!searchQuery);
|
|
145
|
-
return (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
169
|
+
return (_jsxs(Wrapper, { ref: wrapperRef, children: [_jsxs(Field, { className: className, renderAsTitle: renderAsTitle, htmlFor: id, ...otherProps, children: [_jsxs(Box, { flex: true, alignItems: "center", justifyContent: "flex-start", children: [showIcon && (_jsx(InputLeadingIconContainer, { $size: 20, children: _jsx(FontAwesomeIcon, { icon: faSearch, color: theme.color.text.subtle }) })), _jsx(Input, { id: id, name: name, ref: ref, placeholder: placeholder, $error: otherProps.error, $frontIcon: showIcon, $fallbackStyle: fallbackStyle, autoComplete: "off", role: "combobox", "aria-autocomplete": "list", "aria-expanded": hasResults, "aria-controls": hasResults ? listboxId : undefined, "aria-activedescendant": hasHighlight ? getOptionId(highlightedIndex) : undefined, value: displayedInputText, onFocus: handleClick, onChange: handleInputChange, selected: isSelected, onClick: handleClick, onKeyDown: handleKeyDown, onBlur: (e) => {
|
|
170
|
+
onBlur?.(e);
|
|
171
|
+
} }), _jsxs(Icons, { flex: true, alignItems: "center", gap: "space.100", $clearSearch: showClearSearchButton, children: [showClearSearchButton && (_jsx(IconContainer, { title: "Clear search", onClick: handleClearSearch, type: "button", as: "button", $size: 20, style: { cursor: 'pointer' }, children: _jsx(FontAwesomeIcon, { icon: faXmark, color: theme.color.illustration.neutral[400] }) })), _jsx(Line, {}), _jsx(IconContainer, { type: "button", title: "icon-button", as: "button", onClick: handleCaretClick, $size: 20, children: _jsx(FontAwesomeIcon, { style: {
|
|
172
|
+
rotate: showOptions ? '180deg' : '0deg',
|
|
173
|
+
}, icon: faChevronDown, color: theme.color.illustration.neutral[400] }) })] })] }), showOptions && (_jsx(SearchOptions, { displayedList: filteredList, selectedValue: selectedValue, highlightedIndex: highlightedIndex, setHighlightedIndex: setHighlightedIndex, onKeyDown: handleKeyDown, listboxId: listboxId, listboxLabel: otherProps.label ?? placeholder ?? 'Search results', getOptionId: getOptionId, searchTerm: searchQuery || '', onSelect: handleSelect, positionRelative: resultsRelativePosition, resultsBorder: resultsBorder, onNotFound: onNotFound, onEmptyMessageChange: setEmptyMessage, notFoundComponent: notFoundComponent?.(searchQuery ?? '') }))] }), _jsx(LiveRegion, { role: "status", children: noResults ? emptyMessage : '' })] }));
|
|
150
174
|
});
|
|
151
175
|
const Wrapper = styled(Box) `
|
|
152
176
|
position: relative;
|
|
153
177
|
`;
|
|
178
|
+
const LiveRegion = styled.div `
|
|
179
|
+
${visuallyHidden}
|
|
180
|
+
`;
|
|
154
181
|
const Line = styled(Box) `
|
|
155
182
|
background: ${({ theme }) => theme.color.border.subtle};
|
|
156
183
|
height: 24px;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchInput.js","sourceRoot":"","sources":["../../src/SearchInput/SearchInput.tsx"],"names":[],"mappings":"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","sourcesContent":["import Fuse, { IFuseOptions } from 'fuse.js'\nimport {\n ChangeEvent,\n FocusEvent,\n ReactNode,\n forwardRef,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport styled, { useTheme } from 'styled-components'\nimport { Box } from '../Box'\nimport { Field } from '../fields/Field'\nimport { CommonFieldProps } from '../fields/commonFieldTypes'\nimport {\n Input,\n InputLeadingIconContainer,\n} from '../fields/components/CommonInput'\nimport { useOnClickOutside } from '../hooks'\nimport { useUniqueId } from '../utils/id'\nimport { useControllableState } from '../utils/useControlledState'\nimport { SearchOptions } from './components/SearchOptions'\nimport { IconContainer } from '../sharedStyles/shared.styles'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport {\n faXmark,\n faChevronDown,\n faSearch,\n} from '@awesome.me/kit-46ca99185c/icons/classic/regular'\n\nexport type SearchInputItem = {\n label: string\n value: string\n tags?: any[]\n}\n\nexport interface SearchInputProps extends CommonFieldProps {\n /** Optional input className */\n name?: string\n /** Optional placeholder text */\n placeholder?: string\n /** List of input items to search on*/\n searchList: SearchInputItem[]\n /** callback to handle found item click */\n onFound: (element: string) => void\n /** optional callback to run when no results found */\n onNotFound?: (searchTerm: string) => void\n /** optional Component to render when no results found */\n notFoundComponent?: (searchTerm: string) => ReactNode\n /** optional boolean to show search icon */\n showIcon?: boolean\n /** optional boolean to show a clear search button */\n clearSearch?: boolean\n /** Optional callback to run on blur */\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n /** Optional default value for input */\n value?: string\n /** Optional boolean to move results to a realtive position */\n resultsRelativePosition?: boolean\n /** optional boolean to add border to results */\n resultsBorder?: boolean\n /** optional boolean to enable fuzzy search via fuse.js */\n enableFuzzySearch?: boolean\n /** optional config of fuzzy search\n * passing a value to this prop, automatically enables fuzzy search\n */\n fuzzySearchOptions?: IFuseOptions<SearchInputItem>\n}\n\nconst defaultFuzzySearchOptions = {\n keys: [\n {\n name: 'label',\n weight: 0.6,\n },\n {\n name: 'tags',\n weight: 0.4,\n },\n ],\n findAllMatches: true,\n minMatchCharLength: 1,\n location: 0,\n threshold: 0.45,\n distance: 55,\n}\n\nexport const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(\n function SearchInput(\n {\n id: idProp,\n name = 'search_input',\n className = '',\n placeholder,\n searchList,\n showIcon = false,\n renderAsTitle = false,\n value,\n onBlur,\n onFound,\n onNotFound,\n notFoundComponent,\n fallbackStyle,\n resultsRelativePosition = false,\n resultsBorder = true,\n enableFuzzySearch = false,\n fuzzySearchOptions,\n clearSearch,\n ...otherProps\n },\n ref,\n ) {\n const wrapperRef = useRef(null)\n const theme = useTheme()\n const id = useUniqueId(idProp)\n const [showOptions, setShowOptions] = useState(false)\n const [selectedValue, setSelectedValue] = useControllableState<\n string | null\n >({\n initialState: null,\n stateProp: value,\n })\n const [searchQuery, setSearchQuery] = useState<string | null>(null)\n const [highlightedIndex, setHighlightedIndex] = useState(-1)\n\n const selectedValueLabel = searchList.find(\n (option) =>\n option.label === selectedValue || option.value === selectedValue,\n )?.label\n\n const handleBlur = () => {\n if (selectedValue) {\n setSearchQuery(selectedValueLabel ?? null)\n } else if (!selectedValue) {\n setSearchQuery(null)\n }\n }\n\n useOnClickOutside({\n ref: wrapperRef,\n callback: () => {\n handleBlur()\n setShowOptions(false)\n },\n })\n\n // oxlint-disable-next-line react/react-compiler\n const fuse = useMemo(() => {\n return new Fuse(searchList, {\n ...defaultFuzzySearchOptions,\n ...fuzzySearchOptions,\n })\n }, [searchList])\n\n // oxlint-disable-next-line react/react-compiler\n const filteredList = useMemo(() => {\n if (searchQuery === null || searchQuery === '') {\n return searchList\n }\n\n if (enableFuzzySearch || !!fuzzySearchOptions) {\n return fuse.search(searchQuery).map(({ item }) => item)\n }\n\n return searchList.filter(({ label }) =>\n label.toLowerCase().includes(searchQuery.toLocaleLowerCase()),\n )\n // oxlint-disable-next-line react/react-compiler\n }, [searchQuery, enableFuzzySearch, !!fuzzySearchOptions])\n\n const getDisplayedInputText = () => {\n if (searchQuery !== null) {\n return searchQuery\n }\n if (selectedValue !== null) {\n return selectedValueLabel || ''\n }\n return ''\n }\n\n const isSelected = selectedValue !== null\n const displayedInputText = getDisplayedInputText()\n\n const updateSearchQuery = (query: string | null) => {\n setSearchQuery(query)\n setHighlightedIndex(-1)\n\n if (query === null) {\n setSelectedValue(null)\n setShowOptions(false)\n } else {\n setShowOptions(true)\n }\n }\n\n const handleClick = () => {\n setShowOptions(true)\n if (searchQuery !== null) {\n updateSearchQuery(searchQuery)\n setShowOptions(true)\n } else if (selectedValue !== null && searchQuery === null) {\n setSearchQuery(selectedValueLabel || null)\n setShowOptions(true)\n }\n }\n\n const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {\n const nextValue = event.currentTarget.value\n updateSearchQuery(nextValue)\n }\n\n const handleSelect = (nextValue: SearchInputItem): void => {\n updateSearchQuery(null)\n setSelectedValue(nextValue.label)\n onFound(nextValue.value)\n }\n\n const handleClearSearch = () => {\n updateSearchQuery(null)\n setSelectedValue(null)\n onFound('')\n }\n\n const handleCaretClick = () => {\n setShowOptions(!showOptions)\n }\n\n const handleKeyDown = (event: {\n key: string\n preventDefault: () => void\n }) => {\n if (event.key === 'Enter' && highlightedIndex !== -1) {\n event.preventDefault()\n const focusedItem = filteredList[highlightedIndex]\n handleSelect(focusedItem)\n } else if (event.key === 'ArrowDown') {\n event.preventDefault()\n const nextIndex = (highlightedIndex + 1) % filteredList.length\n setHighlightedIndex(nextIndex)\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n const prevIndex =\n (highlightedIndex - 1 + filteredList.length) % filteredList.length\n setHighlightedIndex(prevIndex)\n }\n }\n\n const showClearSearchButton =\n !!clearSearch && (!!value || !!selectedValue || !!searchQuery)\n\n return (\n <Wrapper ref={wrapperRef}>\n <Field\n className={className}\n renderAsTitle={renderAsTitle}\n htmlFor={id}\n {...otherProps}\n >\n <Box flex alignItems=\"center\" justifyContent=\"flex-start\">\n {showIcon && (\n <InputLeadingIconContainer $size={20}>\n <FontAwesomeIcon\n icon={faSearch}\n color={theme.color.text.subtle}\n />\n </InputLeadingIconContainer>\n )}\n <Input\n id={id}\n name={name}\n ref={ref}\n placeholder={placeholder}\n $error={otherProps.error}\n $frontIcon={showIcon}\n $fallbackStyle={fallbackStyle}\n autoComplete=\"off\"\n value={displayedInputText}\n onFocus={handleClick}\n onChange={handleInputChange}\n selected={isSelected}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n onBlur={(e) => {\n onBlur?.(e)\n }}\n />\n <Icons\n flex\n alignItems=\"center\"\n gap=\"space.100\"\n $clearSearch={showClearSearchButton}\n >\n {showClearSearchButton && (\n <IconContainer\n title=\"Clear search\"\n onClick={handleClearSearch}\n type=\"button\"\n as=\"button\"\n $size={20}\n style={{ cursor: 'pointer' }}\n >\n <FontAwesomeIcon\n icon={faXmark}\n color={theme.color.illustration.neutral[400]}\n />\n </IconContainer>\n )}\n <Line />\n <IconContainer\n type=\"button\"\n title=\"icon-button\"\n as=\"button\"\n onClick={handleCaretClick}\n $size={20}\n >\n <FontAwesomeIcon\n style={{\n rotate: showOptions ? '180deg' : '0deg',\n }}\n icon={faChevronDown}\n color={theme.color.illustration.neutral[400]}\n />\n </IconContainer>\n </Icons>\n </Box>\n\n {showOptions && (\n <SearchOptions\n displayedList={filteredList}\n selectedValue={selectedValue}\n highlightedIndex={highlightedIndex}\n setHighlightedIndex={setHighlightedIndex}\n onKeyDown={handleKeyDown}\n searchTerm={searchQuery || ''}\n onSelect={handleSelect}\n positionRelative={resultsRelativePosition}\n resultsBorder={resultsBorder}\n onNotFound={onNotFound}\n notFoundComponent={notFoundComponent?.(searchQuery ?? '')}\n />\n )}\n </Field>\n </Wrapper>\n )\n },\n)\n\nconst Wrapper = styled(Box)`\n position: relative;\n`\n\nconst Line = styled(Box)`\n background: ${({ theme }) => theme.color.border.subtle};\n height: 24px;\n width: 1px;\n`\n\nconst Icons = styled(Box)<{ $clearSearch: boolean }>`\n position: relative;\n right: ${({ $clearSearch }) => ($clearSearch ? '80px' : '48px')};\n margin-right: ${({ $clearSearch }) => ($clearSearch ? '-80px' : '-48px')};\n`\n"]}
|
|
1
|
+
{"version":3,"file":"SearchInput.js","sourceRoot":"","sources":["../../src/SearchInput/SearchInput.tsx"],"names":[],"mappings":"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","sourcesContent":["import Fuse, { IFuseOptions } from 'fuse.js'\nimport {\n ChangeEvent,\n FocusEvent,\n ReactNode,\n forwardRef,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport styled, { useTheme } from 'styled-components'\nimport { Box } from '../Box'\nimport { Field } from '../fields/Field'\nimport { CommonFieldProps } from '../fields/commonFieldTypes'\nimport {\n Input,\n InputLeadingIconContainer,\n} from '../fields/components/CommonInput'\nimport { useOnClickOutside } from '../hooks'\nimport { useUniqueId } from '../utils/id'\nimport { useControllableState } from '../utils/useControlledState'\nimport { SearchKeyEvent, SearchOptions } from './components/SearchOptions'\nimport { visuallyHidden } from '../utils/visuallyHidden'\nimport { IconContainer } from '../sharedStyles/shared.styles'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport {\n faXmark,\n faChevronDown,\n faSearch,\n} from '@awesome.me/kit-46ca99185c/icons/classic/regular'\n\nexport type SearchInputItem = {\n label: string\n value: string\n tags?: any[]\n}\n\nexport interface SearchInputProps extends CommonFieldProps {\n /** Optional input className */\n name?: string\n /** Optional placeholder text */\n placeholder?: string\n /** List of input items to search on*/\n searchList: SearchInputItem[]\n /** callback to handle found item click */\n onFound: (element: string) => void\n /** optional callback to run when no results found */\n onNotFound?: (searchTerm: string) => void\n /** optional Component to render when no results found */\n notFoundComponent?: (searchTerm: string) => ReactNode\n /** optional boolean to show search icon */\n showIcon?: boolean\n /** optional boolean to show a clear search button */\n clearSearch?: boolean\n /** Optional callback to run on blur */\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n /** Optional default value for input */\n value?: string\n /** Optional boolean to move results to a realtive position */\n resultsRelativePosition?: boolean\n /** optional boolean to add border to results */\n resultsBorder?: boolean\n /** optional boolean to enable fuzzy search via fuse.js */\n enableFuzzySearch?: boolean\n /** optional config of fuzzy search\n * passing a value to this prop, automatically enables fuzzy search\n */\n fuzzySearchOptions?: IFuseOptions<SearchInputItem>\n}\n\nconst defaultFuzzySearchOptions = {\n keys: [\n {\n name: 'label',\n weight: 0.6,\n },\n {\n name: 'tags',\n weight: 0.4,\n },\n ],\n findAllMatches: true,\n minMatchCharLength: 1,\n location: 0,\n threshold: 0.45,\n distance: 55,\n}\n\nexport const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(\n function SearchInput(\n {\n id: idProp,\n name = 'search_input',\n className = '',\n placeholder,\n searchList,\n showIcon = false,\n renderAsTitle = false,\n value,\n onBlur,\n onFound,\n onNotFound,\n notFoundComponent,\n fallbackStyle,\n resultsRelativePosition = false,\n resultsBorder = true,\n enableFuzzySearch = false,\n fuzzySearchOptions,\n clearSearch,\n ...otherProps\n },\n ref,\n ) {\n const wrapperRef = useRef(null)\n const theme = useTheme()\n const id = useUniqueId(idProp)\n const [showOptions, setShowOptions] = useState(false)\n const [selectedValue, setSelectedValue] = useControllableState<\n string | null\n >({\n initialState: null,\n stateProp: value,\n })\n const [searchQuery, setSearchQuery] = useState<string | null>(null)\n const [highlightedIndex, setHighlightedIndex] = useState(-1)\n const [emptyMessage, setEmptyMessage] = useState('')\n\n const selectedValueLabel = searchList.find(\n (option) =>\n option.label === selectedValue || option.value === selectedValue,\n )?.label\n\n const handleBlur = () => {\n setSearchQuery(null)\n }\n\n // Every way of closing the list goes through here, so a reopened list\n // never starts with a stale highlighted option.\n const closeOptions = () => {\n setShowOptions(false)\n setHighlightedIndex(-1)\n }\n\n useOnClickOutside({\n ref: wrapperRef,\n callback: () => {\n handleBlur()\n closeOptions()\n },\n })\n\n // oxlint-disable-next-line react/react-compiler\n const fuse = useMemo(() => {\n return new Fuse(searchList, {\n ...defaultFuzzySearchOptions,\n ...fuzzySearchOptions,\n })\n }, [searchList])\n\n // oxlint-disable-next-line react/react-compiler\n const filteredList = useMemo(() => {\n if (searchQuery === null || searchQuery === '') {\n return searchList\n }\n\n if (enableFuzzySearch || !!fuzzySearchOptions) {\n return fuse.search(searchQuery).map(({ item }) => item)\n }\n\n return searchList.filter(({ label }) =>\n label.toLowerCase().includes(searchQuery.toLocaleLowerCase()),\n )\n // oxlint-disable-next-line react/react-compiler\n }, [searchQuery, enableFuzzySearch, !!fuzzySearchOptions])\n\n const getDisplayedInputText = () => {\n if (searchQuery !== null) {\n return searchQuery\n }\n if (selectedValue !== null) {\n return selectedValueLabel || ''\n }\n return ''\n }\n\n const isSelected = selectedValue !== null\n const displayedInputText = getDisplayedInputText()\n\n const updateSearchQuery = (query: string | null) => {\n setSearchQuery(query)\n setHighlightedIndex(-1)\n\n if (query === null) {\n setSelectedValue(null)\n closeOptions()\n } else {\n setShowOptions(true)\n }\n }\n\n const handleClick = () => {\n setShowOptions(true)\n if (searchQuery !== null) {\n updateSearchQuery(searchQuery)\n setShowOptions(true)\n }\n }\n\n const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {\n const nextValue = event.currentTarget.value\n updateSearchQuery(nextValue)\n }\n\n const handleSelect = (nextValue: SearchInputItem): void => {\n updateSearchQuery(null)\n setSelectedValue(nextValue.label)\n onFound(nextValue.value)\n }\n\n const handleClearSearch = () => {\n updateSearchQuery(null)\n setSelectedValue(null)\n onFound('')\n }\n\n const handleCaretClick = () => {\n if (showOptions) {\n closeOptions()\n } else {\n setShowOptions(true)\n }\n }\n\n const listboxId = `${id}-listbox`\n const getOptionId = (index: number) => `${id}-option-${index}`\n const hasResults = showOptions && filteredList.length > 0\n const hasHighlight =\n hasResults &&\n highlightedIndex >= 0 &&\n highlightedIndex < filteredList.length\n const noResults = showOptions && filteredList.length === 0\n\n const handleKeyDown = (event: SearchKeyEvent) => {\n if (event.key === 'Escape') {\n if (!showOptions) return\n event.preventDefault()\n event.stopPropagation()\n closeOptions()\n } else if (event.key === 'Enter') {\n if (hasHighlight) {\n event.preventDefault()\n handleSelect(filteredList[highlightedIndex])\n }\n } else if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showOptions) {\n setShowOptions(true)\n } else if (filteredList.length > 0) {\n setHighlightedIndex((highlightedIndex + 1) % filteredList.length)\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n if (showOptions && filteredList.length > 0) {\n setHighlightedIndex(\n (highlightedIndex - 1 + filteredList.length) % filteredList.length,\n )\n }\n }\n }\n\n const showClearSearchButton =\n !!clearSearch && (!!value || !!selectedValue || !!searchQuery)\n\n return (\n <Wrapper ref={wrapperRef}>\n <Field\n className={className}\n renderAsTitle={renderAsTitle}\n htmlFor={id}\n {...otherProps}\n >\n <Box flex alignItems=\"center\" justifyContent=\"flex-start\">\n {showIcon && (\n <InputLeadingIconContainer $size={20}>\n <FontAwesomeIcon\n icon={faSearch}\n color={theme.color.text.subtle}\n />\n </InputLeadingIconContainer>\n )}\n <Input\n id={id}\n name={name}\n ref={ref}\n placeholder={placeholder}\n $error={otherProps.error}\n $frontIcon={showIcon}\n $fallbackStyle={fallbackStyle}\n autoComplete=\"off\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={hasResults}\n aria-controls={hasResults ? listboxId : undefined}\n aria-activedescendant={\n hasHighlight ? getOptionId(highlightedIndex) : undefined\n }\n value={displayedInputText}\n onFocus={handleClick}\n onChange={handleInputChange}\n selected={isSelected}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n onBlur={(e) => {\n onBlur?.(e)\n }}\n />\n <Icons\n flex\n alignItems=\"center\"\n gap=\"space.100\"\n $clearSearch={showClearSearchButton}\n >\n {showClearSearchButton && (\n <IconContainer\n title=\"Clear search\"\n onClick={handleClearSearch}\n type=\"button\"\n as=\"button\"\n $size={20}\n style={{ cursor: 'pointer' }}\n >\n <FontAwesomeIcon\n icon={faXmark}\n color={theme.color.illustration.neutral[400]}\n />\n </IconContainer>\n )}\n <Line />\n <IconContainer\n type=\"button\"\n title=\"icon-button\"\n as=\"button\"\n onClick={handleCaretClick}\n $size={20}\n >\n <FontAwesomeIcon\n style={{\n rotate: showOptions ? '180deg' : '0deg',\n }}\n icon={faChevronDown}\n color={theme.color.illustration.neutral[400]}\n />\n </IconContainer>\n </Icons>\n </Box>\n\n {showOptions && (\n <SearchOptions\n displayedList={filteredList}\n selectedValue={selectedValue}\n highlightedIndex={highlightedIndex}\n setHighlightedIndex={setHighlightedIndex}\n onKeyDown={handleKeyDown}\n listboxId={listboxId}\n listboxLabel={otherProps.label ?? placeholder ?? 'Search results'}\n getOptionId={getOptionId}\n searchTerm={searchQuery || ''}\n onSelect={handleSelect}\n positionRelative={resultsRelativePosition}\n resultsBorder={resultsBorder}\n onNotFound={onNotFound}\n onEmptyMessageChange={setEmptyMessage}\n notFoundComponent={notFoundComponent?.(searchQuery ?? '')}\n />\n )}\n </Field>\n <LiveRegion role=\"status\">{noResults ? emptyMessage : ''}</LiveRegion>\n </Wrapper>\n )\n },\n)\n\nconst Wrapper = styled(Box)`\n position: relative;\n`\n\nconst LiveRegion = styled.div`\n ${visuallyHidden}\n`\n\nconst Line = styled(Box)`\n background: ${({ theme }) => theme.color.border.subtle};\n height: 24px;\n width: 1px;\n`\n\nconst Icons = styled(Box)<{ $clearSearch: boolean }>`\n position: relative;\n right: ${({ $clearSearch }) => ($clearSearch ? '80px' : '48px')};\n margin-right: ${({ $clearSearch }) => ($clearSearch ? '-80px' : '-48px')};\n`\n"]}
|
|
@@ -1,12 +1,45 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect } from 'react';
|
|
3
|
-
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
3
|
+
const DEFAULT_MESSAGE = 'No results';
|
|
4
|
+
const BLOCK_SELECTOR = 'address, article, aside, blockquote, dd, div, dl, dt, figcaption, footer, h1, h2, h3, h4, h5, h6, header, li, ol, p, section, ul';
|
|
5
|
+
// Reads the element's text as a screen reader would: skipping hidden parts,
|
|
6
|
+
// and adding a space where it moves to another block or line, so a heading
|
|
7
|
+
// and a hint don't run together.
|
|
8
|
+
const getText = (element) => {
|
|
9
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
|
|
10
|
+
let text = '';
|
|
11
|
+
let previousBlock = null;
|
|
12
|
+
while (walker.nextNode()) {
|
|
13
|
+
const node = walker.currentNode;
|
|
14
|
+
if (node.nodeName === 'BR') {
|
|
15
|
+
text += ' ';
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (node.nodeType !== Node.TEXT_NODE)
|
|
19
|
+
continue;
|
|
20
|
+
const parent = node.parentElement;
|
|
21
|
+
if (parent?.closest('[aria-hidden="true"], [hidden]'))
|
|
22
|
+
continue;
|
|
23
|
+
const block = parent?.closest(BLOCK_SELECTOR) ?? null;
|
|
24
|
+
if (previousBlock && block !== previousBlock) {
|
|
25
|
+
text += ' ';
|
|
26
|
+
}
|
|
27
|
+
text += node.textContent ?? '';
|
|
28
|
+
previousBlock = block;
|
|
29
|
+
}
|
|
30
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
31
|
+
};
|
|
32
|
+
export const EmptyResults = ({ onNotFound, onMessageChange, searchTerm, notFoundComponent, }) => {
|
|
33
|
+
const ref = useRef(null);
|
|
4
34
|
useEffect(() => {
|
|
5
35
|
onNotFound?.(searchTerm);
|
|
6
36
|
}, []);
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
37
|
+
// Report the text people see, so the live region announces a custom
|
|
38
|
+
// notFoundComponent too. A layout effect updates it before paint.
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const text = ref.current ? getText(ref.current) : '';
|
|
41
|
+
onMessageChange(text || DEFAULT_MESSAGE);
|
|
42
|
+
}, [searchTerm, notFoundComponent, onMessageChange]);
|
|
43
|
+
return _jsx("li", { ref: ref, children: notFoundComponent || DEFAULT_MESSAGE });
|
|
11
44
|
};
|
|
12
45
|
//# sourceMappingURL=EmptyResults.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmptyResults.js","sourceRoot":"","sources":["../../../src/SearchInput/components/EmptyResults.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"EmptyResults.js","sourceRoot":"","sources":["../../../src/SearchInput/components/EmptyResults.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAiB,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzE,MAAM,eAAe,GAAG,YAAY,CAAA;AASpC,MAAM,cAAc,GAClB,kIAAkI,CAAA;AAEpI,4EAA4E;AAC5E,2EAA2E;AAC3E,iCAAiC;AACjC,MAAM,OAAO,GAAG,CAAC,OAAoB,EAAE,EAAE;IACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CACtC,OAAO,EACP,UAAU,CAAC,SAAS,GAAG,UAAU,CAAC,YAAY,CAC/C,CAAA;IACD,IAAI,IAAI,GAAG,EAAE,CAAA;IACb,IAAI,aAAa,GAAmB,IAAI,CAAA;IACxC,OAAO,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAA;QAC/B,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC3B,IAAI,IAAI,GAAG,CAAA;YACX,SAAQ;QACV,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS;YAAE,SAAQ;QAE9C,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAA;QACjC,IAAI,MAAM,EAAE,OAAO,CAAC,gCAAgC,CAAC;YAAE,SAAQ;QAE/D,MAAM,KAAK,GAAG,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,IAAI,IAAI,CAAA;QACrD,IAAI,aAAa,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;YAC7C,IAAI,IAAI,GAAG,CAAA;QACb,CAAC;QACD,IAAI,IAAI,IAAI,CAAC,WAAW,IAAI,EAAE,CAAA;QAC9B,aAAa,GAAG,KAAK,CAAA;IACvB,CAAC;IACD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;AACzC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAc,CAAC,EACtC,UAAU,EACV,eAAe,EACf,UAAU,EACV,iBAAiB,GAClB,EAAE,EAAE;IACH,MAAM,GAAG,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAA;IAEvC,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,EAAE,CAAC,UAAU,CAAC,CAAA;IAC1B,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,oEAAoE;IACpE,kEAAkE;IAClE,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACpD,eAAe,CAAC,IAAI,IAAI,eAAe,CAAC,CAAA;IAC1C,CAAC,EAAE,CAAC,UAAU,EAAE,iBAAiB,EAAE,eAAe,CAAC,CAAC,CAAA;IAEpD,OAAO,aAAI,GAAG,EAAE,GAAG,YAAG,iBAAiB,IAAI,eAAe,GAAM,CAAA;AAClE,CAAC,CAAA","sourcesContent":["import { FC, ReactNode, useEffect, useLayoutEffect, useRef } from 'react'\n\nconst DEFAULT_MESSAGE = 'No results'\n\ninterface Props {\n onNotFound?: (searchTerm: string) => void\n onMessageChange: (message: string) => void\n searchTerm: string\n notFoundComponent?: ReactNode\n}\n\nconst BLOCK_SELECTOR =\n 'address, article, aside, blockquote, dd, div, dl, dt, figcaption, footer, h1, h2, h3, h4, h5, h6, header, li, ol, p, section, ul'\n\n// Reads the element's text as a screen reader would: skipping hidden parts,\n// and adding a space where it moves to another block or line, so a heading\n// and a hint don't run together.\nconst getText = (element: HTMLElement) => {\n const walker = document.createTreeWalker(\n element,\n NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,\n )\n let text = ''\n let previousBlock: Element | null = null\n while (walker.nextNode()) {\n const node = walker.currentNode\n if (node.nodeName === 'BR') {\n text += ' '\n continue\n }\n if (node.nodeType !== Node.TEXT_NODE) continue\n\n const parent = node.parentElement\n if (parent?.closest('[aria-hidden=\"true\"], [hidden]')) continue\n\n const block = parent?.closest(BLOCK_SELECTOR) ?? null\n if (previousBlock && block !== previousBlock) {\n text += ' '\n }\n text += node.textContent ?? ''\n previousBlock = block\n }\n return text.replace(/\\s+/g, ' ').trim()\n}\n\nexport const EmptyResults: FC<Props> = ({\n onNotFound,\n onMessageChange,\n searchTerm,\n notFoundComponent,\n}) => {\n const ref = useRef<HTMLLIElement>(null)\n\n useEffect(() => {\n onNotFound?.(searchTerm)\n }, [])\n\n // Report the text people see, so the live region announces a custom\n // notFoundComponent too. A layout effect updates it before paint.\n useLayoutEffect(() => {\n const text = ref.current ? getText(ref.current) : ''\n onMessageChange(text || DEFAULT_MESSAGE)\n }, [searchTerm, notFoundComponent, onMessageChange])\n\n return <li ref={ref}>{notFoundComponent || DEFAULT_MESSAGE}</li>\n}\n"]}
|
|
@@ -3,19 +3,25 @@ type Option = {
|
|
|
3
3
|
label: string;
|
|
4
4
|
value: string;
|
|
5
5
|
};
|
|
6
|
+
export type SearchKeyEvent = {
|
|
7
|
+
key: string;
|
|
8
|
+
preventDefault: () => void;
|
|
9
|
+
stopPropagation: () => void;
|
|
10
|
+
};
|
|
6
11
|
type SearchOptionsProps = {
|
|
7
12
|
displayedList: Array<Option>;
|
|
8
13
|
selectedValue: string | null;
|
|
9
14
|
highlightedIndex: number;
|
|
10
15
|
setHighlightedIndex: (arg: number) => void;
|
|
11
16
|
onSelect: (option: Option) => void;
|
|
12
|
-
onKeyDown: (e:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
onKeyDown: (e: SearchKeyEvent) => void;
|
|
18
|
+
listboxId: string;
|
|
19
|
+
listboxLabel: string;
|
|
20
|
+
getOptionId: (index: number) => string;
|
|
16
21
|
positionRelative: boolean;
|
|
17
22
|
resultsBorder: boolean;
|
|
18
23
|
onNotFound?: (searchTerm: string) => void;
|
|
24
|
+
onEmptyMessageChange: (message: string) => void;
|
|
19
25
|
searchTerm: string;
|
|
20
26
|
notFoundComponent?: ReactNode;
|
|
21
27
|
};
|
|
@@ -4,7 +4,7 @@ import styled, { css } from 'styled-components';
|
|
|
4
4
|
import { Box } from '../../Box';
|
|
5
5
|
import { Icon } from '../../Icon';
|
|
6
6
|
import { EmptyResults } from './EmptyResults';
|
|
7
|
-
export const SearchOptions = ({ displayedList, selectedValue, highlightedIndex, setHighlightedIndex, onSelect, onKeyDown, positionRelative, resultsBorder, onNotFound, searchTerm, notFoundComponent, }) => {
|
|
7
|
+
export const SearchOptions = ({ displayedList, selectedValue, highlightedIndex, setHighlightedIndex, onSelect, onKeyDown, listboxId, listboxLabel, getOptionId, positionRelative, resultsBorder, onNotFound, onEmptyMessageChange, searchTerm, notFoundComponent, }) => {
|
|
8
8
|
const itemRefs = useRef([]);
|
|
9
9
|
const resultsListRef = useRef(null);
|
|
10
10
|
useEffect(() => {
|
|
@@ -25,16 +25,16 @@ export const SearchOptions = ({ displayedList, selectedValue, highlightedIndex,
|
|
|
25
25
|
}
|
|
26
26
|
setHighlightedIndex(-1);
|
|
27
27
|
}, [searchTerm, setHighlightedIndex]);
|
|
28
|
-
return (_jsx(BoxWithPositionRelative, { children: _jsx(StyledResultsContainer, { $positionRelative: positionRelative, children: _jsx(ResultsList, { ref: resultsListRef, $resultsBorder: resultsBorder, onKeyDown: onKeyDown, children: displayedList.length ? (
|
|
28
|
+
return (_jsx(BoxWithPositionRelative, { children: _jsx(StyledResultsContainer, { $positionRelative: positionRelative, children: _jsx(ResultsList, { ref: resultsListRef, id: listboxId, role: displayedList.length ? 'listbox' : undefined, "aria-label": displayedList.length ? listboxLabel : undefined, $resultsBorder: resultsBorder, onKeyDown: onKeyDown, children: displayedList.length ? (
|
|
29
29
|
// oxlint-disable-next-line react/react-compiler
|
|
30
30
|
displayedList.map((el, i) => {
|
|
31
31
|
const isSelected = selectedValue === el.label || selectedValue === el.value;
|
|
32
|
-
return (_jsxs(ListButton, { "aria-
|
|
32
|
+
return (_jsxs(ListButton, { id: getOptionId(i), role: "option", "aria-selected": highlightedIndex === i, ref: itemRefs.current[i], onClick: () => onSelect(el), $isSelected: isSelected, $showBg: highlightedIndex === i, onMouseEnter: () => {
|
|
33
33
|
setHighlightedIndex(i);
|
|
34
34
|
}, onFocus: () => {
|
|
35
35
|
setHighlightedIndex(i);
|
|
36
36
|
}, children: [el.label, isSelected && (_jsx(Icon, { render: "tick", size: 16, color: "color.surface.brand.300" }))] }, el.label + '_list_item'));
|
|
37
|
-
})) : (_jsx(EmptyResults, { onNotFound: onNotFound, searchTerm: searchTerm, notFoundComponent: notFoundComponent })) }) }) }));
|
|
37
|
+
})) : (_jsx(EmptyResults, { onNotFound: onNotFound, onMessageChange: onEmptyMessageChange, searchTerm: searchTerm, notFoundComponent: notFoundComponent })) }) }) }));
|
|
38
38
|
};
|
|
39
39
|
const StyledResultsContainer = styled.div `
|
|
40
40
|
box-sizing: border-box;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchOptions.js","sourceRoot":"","sources":["../../../src/SearchInput/components/SearchOptions.tsx"],"names":[],"mappings":";AAAA,OAAO,EAA4B,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAC9E,OAAO,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"SearchOptions.js","sourceRoot":"","sources":["../../../src/SearchInput/components/SearchOptions.tsx"],"names":[],"mappings":"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","sourcesContent":["import { FC, ReactNode, RefObject, createRef, useEffect, useRef } from 'react'\nimport styled, { css } from 'styled-components'\nimport { TransientProps } from '../../utils/utilTypes'\nimport { Box } from '../../Box'\nimport { Icon } from '../../Icon'\nimport { EmptyResults } from './EmptyResults'\n\ntype Option = {\n label: string\n value: string\n}\n\nexport type SearchKeyEvent = {\n key: string\n preventDefault: () => void\n stopPropagation: () => void\n}\n\ntype SearchOptionsProps = {\n displayedList: Array<Option>\n selectedValue: string | null\n highlightedIndex: number\n setHighlightedIndex: (arg: number) => void\n onSelect: (option: Option) => void\n onKeyDown: (e: SearchKeyEvent) => void\n listboxId: string\n listboxLabel: string\n getOptionId: (index: number) => string\n positionRelative: boolean\n resultsBorder: boolean\n onNotFound?: (searchTerm: string) => void\n onEmptyMessageChange: (message: string) => void\n searchTerm: string\n notFoundComponent?: ReactNode\n}\n\nexport const SearchOptions: FC<SearchOptionsProps> = ({\n displayedList,\n selectedValue,\n highlightedIndex,\n setHighlightedIndex,\n onSelect,\n onKeyDown,\n listboxId,\n listboxLabel,\n getOptionId,\n positionRelative,\n resultsBorder,\n onNotFound,\n onEmptyMessageChange,\n searchTerm,\n notFoundComponent,\n}) => {\n const itemRefs = useRef<RefObject<HTMLLIElement | null>[]>([])\n const resultsListRef = useRef<HTMLUListElement>(null)\n\n useEffect(() => {\n itemRefs.current = displayedList.map(\n (_, i) => itemRefs.current[i] ?? createRef<HTMLLIElement>(),\n )\n }, [displayedList.length])\n\n useEffect(() => {\n const focusedListItem = itemRefs?.current[highlightedIndex]?.current\n if (highlightedIndex >= 0 && focusedListItem) {\n focusedListItem.scrollIntoView({\n block: 'nearest',\n inline: 'start',\n })\n }\n }, [highlightedIndex])\n\n useEffect(() => {\n if (resultsListRef.current) {\n resultsListRef.current.scrollTop = 0\n }\n setHighlightedIndex(-1)\n }, [searchTerm, setHighlightedIndex])\n\n return (\n <BoxWithPositionRelative>\n <StyledResultsContainer $positionRelative={positionRelative}>\n <ResultsList\n ref={resultsListRef}\n id={listboxId}\n role={displayedList.length ? 'listbox' : undefined}\n aria-label={displayedList.length ? listboxLabel : undefined}\n $resultsBorder={resultsBorder}\n onKeyDown={onKeyDown}\n >\n {displayedList.length ? (\n // oxlint-disable-next-line react/react-compiler\n displayedList.map((el, i) => {\n const isSelected =\n selectedValue === el.label || selectedValue === el.value\n\n return (\n <ListButton\n key={el.label + '_list_item'}\n id={getOptionId(i)}\n role=\"option\"\n aria-selected={highlightedIndex === i}\n ref={itemRefs.current[i]}\n onClick={() => onSelect(el)}\n $isSelected={isSelected}\n $showBg={highlightedIndex === i}\n onMouseEnter={() => {\n setHighlightedIndex(i)\n }}\n onFocus={() => {\n setHighlightedIndex(i)\n }}\n >\n {el.label}\n {isSelected && (\n <Icon\n render=\"tick\"\n size={16}\n color=\"color.surface.brand.300\"\n />\n )}\n </ListButton>\n )\n })\n ) : (\n <EmptyResults\n onNotFound={onNotFound}\n onMessageChange={onEmptyMessageChange}\n searchTerm={searchTerm}\n notFoundComponent={notFoundComponent}\n />\n )}\n </ResultsList>\n </StyledResultsContainer>\n </BoxWithPositionRelative>\n )\n}\n\nconst StyledResultsContainer = styled.div<\n TransientProps<Pick<SearchOptionsProps, 'positionRelative'>>\n>`\n box-sizing: border-box;\n overflow: hidden;\n margin: -16px;\n padding: ${({ theme }) => theme.space[200]};\n ${({ $positionRelative }) => !$positionRelative && 'position: absolute;'}\n width: calc(100% + 32px);\n left: 0px;\n top: -8px;\n\n ul {\n max-height: 192px;\n }\n`\n\nconst ResultsList = styled.ul<\n TransientProps<Pick<SearchOptionsProps, 'resultsBorder'>>\n>`\n position: relative;\n list-style: none;\n overflow-y: auto;\n padding: 0;\n margin: 0;\n background-color: ${({ theme }) => theme.color.background['000']};\n border-radius: 12px;\n margin-top: 14px;\n z-index: 1000;\n border: 1px solid ${({ theme }) => theme.color.border.subtle};\n box-shadow:\n 0px 10px 18px rgba(18, 18, 23, 0.03),\n 0px 5px 8px rgba(18, 18, 23, 0.04),\n 0px 2px 4px rgba(18, 18, 23, 0.04);\n\n li {\n padding: 16px 14px;\n box-sizing: border-box;\n font-size: 16px;\n color: ${({ theme }) => theme.color.text.base};\n cursor: pointer;\n }\n`\n\nconst ListButton = styled.li<{ $isSelected: boolean; $showBg: boolean }>`\n display: flex;\n justify-content: space-between;\n\n &:focus {\n outline: none;\n }\n &:focus-visible {\n outline: none;\n }\n\n ${({ $showBg }) =>\n $showBg &&\n css`\n background-color: ${({ theme }) => theme.color.background[200]};\n `}\n\n ${({ $isSelected }) =>\n $isSelected &&\n css`\n background-color: ${({ theme }) => theme.color.surface.base[300]};\n `}\n`\n\nconst BoxWithPositionRelative = styled(Box)`\n position: relative;\n`\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrshmllw/smores-react",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.10.1",
|
|
4
4
|
"description": "Collection of React components used by Marshmallow Technology",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@commitlint/cli": "^21.2.3",
|
|
70
70
|
"@mrshmllw/campfire": "^7.1.0",
|
|
71
|
-
"@snyk/protect": "^1.
|
|
71
|
+
"@snyk/protect": "^1.1308.0",
|
|
72
72
|
"@storybook/addon-a11y": "^10.6.1",
|
|
73
73
|
"@storybook/addon-coverage": "^3.0.2",
|
|
74
74
|
"@storybook/addon-docs": "^10.6.1",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"@types/react": "^19.3.0",
|
|
83
83
|
"@types/react-dom": "^19.3.0",
|
|
84
84
|
"axe-playwright": "^2.2.2",
|
|
85
|
-
"concurrently": "^10.0.
|
|
85
|
+
"concurrently": "^10.0.6",
|
|
86
86
|
"eslint-plugin-prettier": "^5.5.6",
|
|
87
87
|
"husky": "^9.1.7",
|
|
88
88
|
"jest-styled-components": "^7.4.0",
|