@snack-uikit/search 0.4.13-preview-85c5f47b.0

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/LICENSE +201 -0
  3. package/README.md +74 -0
  4. package/dist/components/Search/Search.d.ts +22 -0
  5. package/dist/components/Search/Search.js +28 -0
  6. package/dist/components/Search/index.d.ts +1 -0
  7. package/dist/components/Search/index.js +1 -0
  8. package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts +23 -0
  9. package/dist/components/SearchAutocomplete/SearchAutocomplete.js +53 -0
  10. package/dist/components/SearchAutocomplete/index.d.ts +1 -0
  11. package/dist/components/SearchAutocomplete/index.js +1 -0
  12. package/dist/components/SearchAutocomplete/styles.module.css +9 -0
  13. package/dist/components/SearchDecorator/SearchDecorator.d.ts +13 -0
  14. package/dist/components/SearchDecorator/SearchDecorator.js +21 -0
  15. package/dist/components/SearchDecorator/index.d.ts +1 -0
  16. package/dist/components/SearchDecorator/index.js +1 -0
  17. package/dist/components/SearchDecorator/styles.module.css +45 -0
  18. package/dist/components/SearchFieldText/SearchFieldText.d.ts +10 -0
  19. package/dist/components/SearchFieldText/SearchFieldText.js +20 -0
  20. package/dist/components/SearchFieldText/index.d.ts +1 -0
  21. package/dist/components/SearchFieldText/index.js +1 -0
  22. package/dist/components/SearchPrivate/SearchPrivate.d.ts +28 -0
  23. package/dist/components/SearchPrivate/SearchPrivate.js +63 -0
  24. package/dist/components/SearchPrivate/index.d.ts +1 -0
  25. package/dist/components/SearchPrivate/index.js +1 -0
  26. package/dist/components/SearchPrivate/styles.module.css +68 -0
  27. package/dist/components/index.d.ts +2 -0
  28. package/dist/components/index.js +2 -0
  29. package/dist/constants.d.ts +18 -0
  30. package/dist/constants.js +19 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/package.json +48 -0
  34. package/src/components/Search/Search.tsx +75 -0
  35. package/src/components/Search/index.ts +1 -0
  36. package/src/components/SearchAutocomplete/SearchAutocomplete.tsx +128 -0
  37. package/src/components/SearchAutocomplete/index.ts +1 -0
  38. package/src/components/SearchAutocomplete/styles.module.scss +13 -0
  39. package/src/components/SearchDecorator/SearchDecorator.tsx +39 -0
  40. package/src/components/SearchDecorator/index.ts +1 -0
  41. package/src/components/SearchDecorator/styles.module.scss +41 -0
  42. package/src/components/SearchFieldText/SearchFieldText.tsx +32 -0
  43. package/src/components/SearchFieldText/index.ts +1 -0
  44. package/src/components/SearchPrivate/SearchPrivate.tsx +130 -0
  45. package/src/components/SearchPrivate/index.ts +1 -0
  46. package/src/components/SearchPrivate/styles.module.scss +43 -0
  47. package/src/components/index.ts +2 -0
  48. package/src/constants.ts +20 -0
  49. package/src/index.ts +1 -0
@@ -0,0 +1,128 @@
1
+ import cn from 'classnames';
2
+ import mergeRefs from 'merge-refs';
3
+ import { forwardRef, KeyboardEvent, useCallback, useRef, useState } from 'react';
4
+
5
+ import { Droplist, ItemSingleProps } from '@snack-uikit/droplist';
6
+
7
+ import { PRIVATE_SEARCH_TEST_IDS, Size, TEST_IDS } from '../../constants';
8
+ import { SearchDecorator } from '../SearchDecorator';
9
+ import { SearchPrivate, SearchPrivateProps } from '../SearchPrivate';
10
+ import styles from './styles.module.scss';
11
+
12
+ export type SearchAutocompleteProps = Omit<SearchPrivateProps, 'onKeyDown'> & {
13
+ /**
14
+ * Элементы выпадающие в Droplist в режиме Autocomplete.
15
+ *
16
+ * На нажатие 'Space', 'Enter' или клике по элементу будет вызываться onSubmit.
17
+ */
18
+ options: Pick<ItemSingleProps, 'option' | 'description' | 'tagLabel' | 'icon' | 'caption' | 'avatar'>[];
19
+ /** Внешний бордер */
20
+ outline?: boolean;
21
+ };
22
+
23
+ export const SearchAutocomplete = forwardRef<HTMLInputElement, SearchAutocompleteProps>(function SearchAutocomplete(
24
+ {
25
+ size = Size.S,
26
+ value,
27
+ onChange,
28
+ placeholder,
29
+ options = [],
30
+ loading,
31
+ outline,
32
+ onSubmit,
33
+ onFocus,
34
+ className,
35
+ ...rest
36
+ },
37
+ ref,
38
+ ) {
39
+ const scrollRef = useRef<HTMLElement>(null);
40
+
41
+ const [isOpen, setIsOpen] = useState(false);
42
+
43
+ const {
44
+ firstElementRefCallback,
45
+ handleDroplistFocusLeave,
46
+ handleDroplistItemClick,
47
+ handleTriggerKeyDown,
48
+ handleDroplistItemKeyDown,
49
+ triggerElementRef,
50
+ } = Droplist.useKeyboardNavigation<HTMLInputElement>({
51
+ setDroplistOpen: setIsOpen,
52
+ triggerType: Droplist.useKeyboardNavigation.triggerTypes.Input,
53
+ });
54
+
55
+ const handleOptionKeyDown = useCallback(
56
+ (event: KeyboardEvent<HTMLButtonElement>) => {
57
+ event.stopPropagation();
58
+
59
+ // ignoring special keys (tab, arrows, backspace and etc.)
60
+ if (event.key.length === 1) {
61
+ triggerElementRef.current?.focus();
62
+ scrollRef.current?.scroll(0, 0);
63
+ }
64
+ },
65
+ [triggerElementRef, scrollRef],
66
+ );
67
+
68
+ const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
69
+ handleTriggerKeyDown(e);
70
+
71
+ if (e.key.length === 1) {
72
+ setIsOpen(true);
73
+ }
74
+ };
75
+
76
+ return (
77
+ <div className={cn(styles.wrap, className)} {...rest}>
78
+ <Droplist
79
+ open={Boolean(triggerElementRef.current?.value) && isOpen && options.length > 0}
80
+ firstElementRefCallback={firstElementRefCallback}
81
+ useScroll
82
+ size={size}
83
+ onFocusLeave={handleDroplistFocusLeave}
84
+ onOpenChange={setIsOpen}
85
+ data-test-id={TEST_IDS.droplist}
86
+ triggerClassName={styles.triggerClassName}
87
+ scrollRef={scrollRef}
88
+ triggerRef={triggerElementRef}
89
+ triggerElement={
90
+ <SearchDecorator
91
+ size={size}
92
+ outline={outline || undefined}
93
+ focused={(isOpen && Boolean(triggerElementRef.current?.value)) || undefined}
94
+ data-test-id={TEST_IDS.decorator}
95
+ >
96
+ <SearchPrivate
97
+ loading={loading}
98
+ value={value}
99
+ onChange={onChange}
100
+ onSubmit={onSubmit}
101
+ placeholder={placeholder}
102
+ ref={mergeRefs(ref, triggerElementRef)}
103
+ onKeyDown={onKeyDown}
104
+ onFocus={onFocus}
105
+ size={size}
106
+ data-test-id={PRIVATE_SEARCH_TEST_IDS.field}
107
+ />
108
+ </SearchDecorator>
109
+ }
110
+ >
111
+ {options.map(item => (
112
+ <Droplist.ItemSingle
113
+ {...item}
114
+ key={item.option}
115
+ onClick={e => {
116
+ handleDroplistItemClick(e);
117
+ onChange?.(item.option);
118
+ onSubmit?.(item.option);
119
+ triggerElementRef.current?.blur();
120
+ }}
121
+ onKeyDown={e => handleDroplistItemKeyDown(e, handleOptionKeyDown)}
122
+ data-test-id={TEST_IDS.option}
123
+ />
124
+ ))}
125
+ </Droplist>
126
+ </div>
127
+ );
128
+ });
@@ -0,0 +1 @@
1
+ export * from './SearchAutocomplete';
@@ -0,0 +1,13 @@
1
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-search';
2
+
3
+ .triggerClassName {
4
+ --offset: #{$space-drop-list-drop-offset};
5
+
6
+ display: flex;
7
+ flex-grow: 1;
8
+ }
9
+
10
+ .wrap {
11
+ display: flex;
12
+ }
13
+
@@ -0,0 +1,39 @@
1
+ import cn from 'classnames';
2
+ import { ReactNode } from 'react';
3
+
4
+ import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
5
+
6
+ import { Size } from '../../constants';
7
+ import { SearchProps } from '../Search';
8
+ import styles from './styles.module.scss';
9
+
10
+ export type SearchDecoratorProps = WithSupportProps<
11
+ {
12
+ children: ReactNode;
13
+ focused?: boolean;
14
+ className?: string;
15
+ } & Pick<SearchProps, 'outline' | 'size'>
16
+ >;
17
+
18
+ export function SearchDecorator({
19
+ children,
20
+ outline,
21
+ size = Size.S,
22
+ focused,
23
+ className,
24
+ ...rest
25
+ }: SearchDecoratorProps) {
26
+ return (
27
+ <div
28
+ className={cn(styles.decorator, className)}
29
+ data-outline={outline || undefined}
30
+ data-size={size}
31
+ data-focused={focused || undefined}
32
+ {...extractSupportProps(rest)}
33
+ >
34
+ {children}
35
+ </div>
36
+ );
37
+ }
38
+
39
+ SearchDecorator.sizes = Size;
@@ -0,0 +1 @@
1
+ export * from './SearchDecorator';
@@ -0,0 +1,41 @@
1
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-search';
2
+ @import "@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element";
3
+
4
+ $sizes: 's', 'm', 'l';
5
+
6
+ .decorator {
7
+ display: flex;
8
+ flex: 1 0 auto;
9
+ align-items: center;
10
+
11
+ box-sizing: border-box;
12
+
13
+ background-color: $sys-neutral-background1-level;
14
+ border-color: transparent;
15
+ border-style: solid;
16
+
17
+ @each $size in $sizes {
18
+ &[data-size='#{$size}'] {
19
+ @include composite-var($search, $size);
20
+ }
21
+ }
22
+
23
+ &[data-outline] {
24
+ border-color: $sys-neutral-decor-default;
25
+ }
26
+
27
+ &:hover {
28
+ background-color: $sys-neutral-background2-level;
29
+ border-color: $sys-primary-decor-hovered;
30
+ }
31
+
32
+ &:focus-within,
33
+ &[data-focused] {
34
+ @include outline-var($container-focused-m);
35
+
36
+ background-color: $sys-neutral-background2-level;
37
+ border-color: $sys-primary-accent-default;
38
+ outline-color: $sys-primary-decor-activated;
39
+ }
40
+ }
41
+
@@ -0,0 +1,32 @@
1
+ import { forwardRef } from 'react';
2
+
3
+ import { PRIVATE_SEARCH_TEST_IDS, Size } from '../../constants';
4
+ import { SearchDecorator } from '../SearchDecorator';
5
+ import { SearchPrivate, SearchPrivateProps } from '../SearchPrivate';
6
+
7
+ export type SearchTextFieldProps = Omit<SearchPrivateProps, 'onKeyDown'> & {
8
+ /** Внешний бордер */
9
+ outline?: boolean;
10
+ };
11
+
12
+ export const SearchFieldText = forwardRef<HTMLInputElement, SearchTextFieldProps>(function SearchFieldText(
13
+ { value, onChange, onBlur, onFocus, size = Size.S, outline, loading, placeholder, onSubmit, className, ...rest },
14
+ ref,
15
+ ) {
16
+ return (
17
+ <SearchDecorator outline={outline} size={size} className={className} {...rest}>
18
+ <SearchPrivate
19
+ ref={ref}
20
+ size={size}
21
+ value={value}
22
+ onChange={onChange}
23
+ onBlur={onBlur}
24
+ onFocus={onFocus}
25
+ onSubmit={onSubmit}
26
+ placeholder={placeholder}
27
+ loading={loading}
28
+ data-test-id={PRIVATE_SEARCH_TEST_IDS.field}
29
+ />
30
+ </SearchDecorator>
31
+ );
32
+ });
@@ -0,0 +1 @@
1
+ export * from './SearchFieldText';
@@ -0,0 +1,130 @@
1
+ import cn from 'classnames';
2
+ import mergeRefs from 'merge-refs';
3
+ import { FocusEvent, forwardRef, KeyboardEvent, useCallback, useMemo, useRef } from 'react';
4
+ import { useUncontrolledProp } from 'uncontrollable';
5
+
6
+ import { SearchSVG } from '@snack-uikit/icons';
7
+ import {
8
+ InputPrivate,
9
+ InputPrivateProps,
10
+ moveCursorToEnd,
11
+ useButtonNavigation,
12
+ useClearButton,
13
+ } from '@snack-uikit/input-private';
14
+ import { Sun } from '@snack-uikit/loaders';
15
+ import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
16
+
17
+ import { PRIVATE_SEARCH_TEST_IDS, Size } from '../../constants';
18
+ import styles from './styles.module.scss';
19
+
20
+ export type SearchPrivateProps = WithSupportProps<
21
+ {
22
+ /** Размер */
23
+ size?: Size;
24
+ /** Состояние загрузки */
25
+ loading?: boolean;
26
+ /** Колбек на подтверждение поиска по строке */
27
+ onSubmit?(value: string): void;
28
+ /** CSS-класс */
29
+ className?: string;
30
+ } & Pick<Partial<InputPrivateProps>, 'value' | 'onChange' | 'placeholder' | 'onFocus' | 'onBlur' | 'onKeyDown'>
31
+ >;
32
+
33
+ const SearchPrivateComponent = forwardRef<HTMLInputElement, SearchPrivateProps>(function SearchPrivate(
34
+ {
35
+ size = Size.S,
36
+ value: valueProp = '',
37
+ onChange: onChangeProp,
38
+ loading,
39
+ placeholder,
40
+ onKeyDown,
41
+ onFocus,
42
+ onBlur,
43
+ onSubmit,
44
+ className,
45
+ ...rest
46
+ },
47
+ ref,
48
+ ) {
49
+ const [value, onValueChange] = useUncontrolledProp(valueProp, '', onChangeProp);
50
+
51
+ const localRef = useRef<HTMLInputElement>(null);
52
+ const clearButtonRef = useRef<HTMLButtonElement>(null);
53
+
54
+ const showClearButton = Boolean(value);
55
+
56
+ const onClear = () => {
57
+ onValueChange('');
58
+
59
+ localRef.current?.focus();
60
+ };
61
+
62
+ const clearButtonSettings = useClearButton({
63
+ clearButtonRef,
64
+ showClearButton,
65
+ size,
66
+ onClear,
67
+ });
68
+
69
+ const { buttons, inputTabIndex, onInputKeyDown } = useButtonNavigation({
70
+ inputRef: localRef,
71
+ buttons: useMemo(() => [clearButtonSettings], [clearButtonSettings]),
72
+ readonly: false,
73
+ submitKeys: ['Enter', 'Space'],
74
+ });
75
+
76
+ const handleKeyDown = useCallback(
77
+ (e: KeyboardEvent<HTMLInputElement>) => {
78
+ onKeyDown && onKeyDown(e);
79
+ onInputKeyDown(e);
80
+
81
+ if (e.key === 'Enter' && localRef.current?.value) {
82
+ onSubmit && onSubmit(localRef.current.value);
83
+ // TODO: think about remove blur behavior
84
+ localRef.current?.blur();
85
+ }
86
+ },
87
+ [onInputKeyDown, onKeyDown, onSubmit],
88
+ );
89
+
90
+ const handleOnFocus = useCallback(
91
+ (e: FocusEvent<HTMLInputElement, Element>) => {
92
+ onFocus && onFocus(e);
93
+ moveCursorToEnd(localRef.current);
94
+ },
95
+ [onFocus],
96
+ );
97
+
98
+ return (
99
+ <div className={cn(styles.container, className)} {...extractSupportProps(rest)} data-size={size}>
100
+ <span className={styles.prefix}>
101
+ {loading ? (
102
+ <Sun data-test-id={PRIVATE_SEARCH_TEST_IDS.iconSun} />
103
+ ) : (
104
+ <SearchSVG data-test-id={PRIVATE_SEARCH_TEST_IDS.iconSearch} />
105
+ )}
106
+ </span>
107
+
108
+ <InputPrivate
109
+ value={value}
110
+ onChange={onValueChange}
111
+ onKeyDown={handleKeyDown}
112
+ onFocus={handleOnFocus}
113
+ onBlur={onBlur}
114
+ tabIndex={inputTabIndex}
115
+ ref={mergeRefs(ref, localRef)}
116
+ placeholder={placeholder}
117
+ type={InputPrivate.types.Text}
118
+ data-test-id={PRIVATE_SEARCH_TEST_IDS.input}
119
+ />
120
+
121
+ <span className={styles.postfix}>{buttons}</span>
122
+ </div>
123
+ );
124
+ });
125
+
126
+ export const SearchPrivate = SearchPrivateComponent as typeof SearchPrivateComponent & {
127
+ sizes: typeof Size;
128
+ };
129
+
130
+ SearchPrivate.sizes = Size;
@@ -0,0 +1 @@
1
+ export * from './SearchPrivate';
@@ -0,0 +1,43 @@
1
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-search';
2
+ @import "@snack-uikit/figma-tokens/build/scss/components/styles-tokens-element";
3
+
4
+ $sizes: 's', 'm', 'l';
5
+
6
+ .container {
7
+ display: flex;
8
+ flex-grow: 1;
9
+ align-items: center;
10
+ box-sizing: border-box;
11
+
12
+ @each $size in $sizes {
13
+ &[data-size='#{$size}'] {
14
+ @include composite-var($search-private, $size);
15
+
16
+ input {
17
+ @include composite-var($theme-variables, 'sans', 'body', $size);
18
+
19
+ cursor: text;
20
+ }
21
+
22
+ svg {
23
+ width: $icon-s !important; /* stylelint-disable-line declaration-no-important */
24
+ height: $icon-s !important; /* stylelint-disable-line declaration-no-important */
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ .prefix {
31
+ display: inline-flex;
32
+ flex-shrink: 0;
33
+ align-items: center;
34
+ justify-content: center;
35
+
36
+ color: $sys-neutral-text-light;
37
+ }
38
+
39
+ .postfix {
40
+ display: inline-flex;
41
+ flex-shrink: 0;
42
+ gap: $space-fields-postfix-gap;
43
+ }
@@ -0,0 +1,2 @@
1
+ export * from './Search';
2
+ export * from './SearchPrivate';
@@ -0,0 +1,20 @@
1
+ export enum Size {
2
+ S = 's',
3
+ M = 'm',
4
+ L = 'l',
5
+ }
6
+
7
+ export const TEST_IDS = {
8
+ main: 'search',
9
+ decorator: 'search__decorator',
10
+ droplist: 'search__droplist',
11
+ option: 'search__option',
12
+ };
13
+
14
+ export const PRIVATE_SEARCH_TEST_IDS = {
15
+ field: 'search__field',
16
+ input: 'search__field-input',
17
+ iconSun: 'search__icon-sun',
18
+ iconSearch: 'search__icon-search',
19
+ buttonClearValue: 'button-clear-value',
20
+ };
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './components';