Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/TokenizedInput/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ This component is for writing queries/filters and working with them as tokens. H
| `transformTokens` | `(tokens: T[]) => TokenizedInputToken<T>[]` | - | Maps raw tokens to internal token shape. |
| `validateToken` | `(token: T) => Partial<Record<keyof T, string>> \| undefined \| false` | - | Validates a token. |
| `formatToken` | `(token: T) => T` | - | Formats a token value before saving. |
| `className` | `string` | - | Wrapper className. |
| `size` | `'m' \| 'l' \| 'xl'` | `'m'` | Control size. |
| `placeholder` | `string \| TokenizedInputTokenPlaceholderGeneratorFn<T>` | - | Placeholder for the new token. |
| `isEditable` | `boolean` | `true` | Whether editing is allowed. |
| `isClearable` | `boolean` | `true` | Whether full clear is allowed. |
Expand All @@ -27,6 +29,10 @@ This component is for writing queries/filters and working with them as tokens. H
| `onBlur` | `() => void` | - | onBlur callback. |
| `shouldAllowBlur` | `(e: React.FocusEvent) => boolean` | `() => true` | Return true to allow blur, false to prevent it. |

### Sizes

The component supports sizes `m`, `l`, and `xl`. The default size is `m`.

### Usage Examples

#### 1. Basic Key-Value Input
Expand Down
131 changes: 107 additions & 24 deletions src/components/TokenizedInput/TokenizedInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@

.gc-tokenized-input {
&__wrapper {
--gc-tokenized-input-token-height: 24px;
--gc-tokenized-input-token-border-radius: var(--g-border-radius-s);
--gc-tokenized-input-clear-button-size: 24px;
--gc-tokenized-input-padding: 2px;
--gc-tokenized-input-field-padding-block: 3px;
--gc-tokenized-input-field-padding-inline: 4px;
--gc-tokenized-input-field-edge-padding-inline: 8px;

position: relative;

display: flex;
gap: 2px;

width: 100%;
padding: 2px;
padding-inline-end: 34px;
padding: var(--gc-tokenized-input-padding);
padding-inline-end: calc(var(--gc-tokenized-input-clear-button-size) + 2px);

border: 1px solid var(--g-color-line-generic);
border-radius: var(--g-border-radius-l);
Expand All @@ -36,19 +44,54 @@
&_focused {
border-color: var(--g-color-line-generic-active);
}

&_size_l {
--gc-tokenized-input-token-height: 30px;
--gc-tokenized-input-token-border-radius: var(--g-border-radius-m);
--gc-tokenized-input-clear-button-size: 28px;
--gc-tokenized-input-field-padding-block: 6px;
--gc-tokenized-input-field-padding-inline: 6px;
--gc-tokenized-input-field-edge-padding-inline: 12px;

font-size: var(--g-text-body-short-font-size);
line-height: var(--g-text-body-short-line-height);
}

&_size_xl {
--gc-tokenized-input-token-height: 38px;
--gc-tokenized-input-token-border-radius: var(--g-border-radius-l);
--gc-tokenized-input-clear-button-size: 36px;
--gc-tokenized-input-field-padding-block: 9px;
--gc-tokenized-input-field-padding-inline: 6px;
--gc-tokenized-input-field-edge-padding-inline: 12px;

border-radius: var(--g-border-radius-xl);

font-size: var(--g-text-body-2-font-size);
line-height: var(--g-text-body-2-line-height);
}
}

&__clear-button {
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
inset-block-start: calc(
var(--gc-tokenized-input-padding) +
(
var(--gc-tokenized-input-token-height) - var(
--gc-tokenized-input-clear-button-size
)
) /
2
);
inset-inline-end: var(--gc-tokenized-input-padding);

display: flex;
justify-content: center;
align-items: center;

height: 100%;
padding: 3px 8px;
width: var(--gc-tokenized-input-clear-button-size);
height: var(--gc-tokenized-input-clear-button-size);
padding: 0;

cursor: pointer;

Expand All @@ -73,9 +116,9 @@
flex-wrap: nowrap;

max-width: 100%;
height: 24px;
height: var(--gc-tokenized-input-token-height);

border-radius: var(--g-border-radius-s);
border-radius: var(--gc-tokenized-input-token-border-radius);
background-color: var(--g-color-base-generic-accent-disabled);

&_new {
Expand Down Expand Up @@ -108,8 +151,8 @@

color: unset;
border: unset;
border-start-end-radius: var(--g-border-radius-s);
border-end-end-radius: var(--g-border-radius-s);
border-start-end-radius: var(--gc-tokenized-input-token-border-radius);
border-end-end-radius: var(--gc-tokenized-input-token-border-radius);
outline: none;
background-color: unset;

Expand All @@ -128,7 +171,8 @@
min-width: 1ch;
max-width: 100%;
height: 100%;
padding: 3px 4px;
padding: var(--gc-tokenized-input-field-padding-block)
var(--gc-tokenized-input-field-padding-inline);

transition: color 0.2s;

Expand All @@ -138,26 +182,26 @@
}

&:first-child {
padding-inline-start: 8px;
padding-inline-start: var(--gc-tokenized-input-field-edge-padding-inline);

border-start-start-radius: var(--g-border-radius-s);
border-end-start-radius: var(--g-border-radius-s);
border-start-start-radius: var(--gc-tokenized-input-token-border-radius);
border-end-start-radius: var(--gc-tokenized-input-token-border-radius);

.gc-tokenized-input__field-input {
inset-inline-start: 8px;
inset-inline-start: var(--gc-tokenized-input-field-edge-padding-inline);

width: calc(100% - 8px);
width: calc(100% - var(--gc-tokenized-input-field-edge-padding-inline));
}
}

&:last-child {
padding-inline-end: 8px;
padding-inline-end: var(--gc-tokenized-input-field-edge-padding-inline);

border-start-end-radius: var(--g-border-radius-s);
border-end-end-radius: var(--g-border-radius-s);
border-start-end-radius: var(--gc-tokenized-input-token-border-radius);
border-end-end-radius: var(--gc-tokenized-input-token-border-radius);

.gc-tokenized-input__field-input {
width: calc(100% - 8px);
width: calc(100% - var(--gc-tokenized-input-field-edge-padding-inline));
}
}

Expand Down Expand Up @@ -200,9 +244,9 @@
&-input {
position: absolute;
inset-block-start: 0;
inset-inline-start: 4px;
inset-inline-start: var(--gc-tokenized-input-field-padding-inline);

width: calc(100% - 4px);
width: calc(100% - var(--gc-tokenized-input-field-padding-inline));
min-width: 2ch;
height: 100%;
padding: 0;
Expand Down Expand Up @@ -257,6 +301,10 @@

&__suggestions-list {
&-wrapper {
--gc-tokenized-input-suggestion-loading-height: 20px;
--gc-tokenized-input-suggestion-padding-block: 2px;
--gc-tokenized-input-suggestion-padding-inline: 12px;

display: flex;
overflow-y: auto;
flex-direction: column;
Expand All @@ -267,7 +315,7 @@

&_loading {
width: 300px;
height: 20px;
height: var(--gc-tokenized-input-suggestion-loading-height);
overflow: hidden;
}

Expand All @@ -280,14 +328,49 @@
width: 100%;
max-width: none;
}

&_size_l {
--gc-tokenized-input-suggestion-loading-height: 32px;
--gc-tokenized-input-suggestion-padding-block: 8px;
--gc-tokenized-input-suggestion-padding-inline: var(--g-spacing-4);

font-size: var(--g-text-body-short-font-size);
line-height: var(--g-text-body-short-line-height);
}

&_size_xl {
--gc-tokenized-input-suggestion-loading-height: 36px;
--gc-tokenized-input-suggestion-padding-block: 8px;
--gc-tokenized-input-suggestion-padding-inline: var(--g-spacing-4);

font-size: var(--g-text-body-2-font-size);
line-height: var(--g-text-body-2-line-height);
}

&_size_l,
&_size_xl {
&.gc-tokenized-input__suggestions-list-wrapper_empty {
.g-list__empty-placeholder {
min-height: 0;
padding: 0;
}
}

&.gc-tokenized-input__suggestions-list-wrapper_loading {
.g-list__item {
height: var(--gc-tokenized-input-suggestion-loading-height);
}
}
}
}

&-item {
overflow-x: hidden;

width: 100%;
margin: 0;
padding: 2px 12px;
padding: var(--gc-tokenized-input-suggestion-padding-block)
var(--gc-tokenized-input-suggestion-padding-inline);

cursor: pointer;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import {TokenizedComponentType} from './types';
const meta: Meta<TokenizedComponentType<Record<string, string>>> = {
title: 'Components/TokenizedInput',
component: TokenizedInput,
argTypes: {
size: {
options: ['m', 'l', 'xl'],
control: {type: 'radio'},
},
},
args: {
size: 'm',
},
parameters: {
disableStrictMode: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function Suggestions({
popupWidth,
popupOffset,
fullWidthSuggestions,
size,
} = suggestionsInfo.state;
const {onApplySuggestion} = suggestionsInfo.callbacks;

Expand All @@ -48,6 +49,7 @@ export function Suggestions({
onApplySuggestion={onApplySuggestion}
selected={selected}
fullWidth={fullWidthSuggestions}
size={size}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import {List} from '@gravity-ui/uikit';

import {b} from '../../constants';
import i18n from '../../i18n';
import type {TokenValueBase, TokenizedSuggestions, TokenizedSuggestionsItem} from '../../types';
import type {
TokenValueBase,
TokenizedInputSize,
TokenizedSuggestions,
TokenizedSuggestionsItem,
} from '../../types';

export type SuggestionsListProps = {
selected: number;
isLoading: boolean;
suggestions: TokenizedSuggestions<TokenValueBase>;
fullWidth?: boolean;
size?: TokenizedInputSize;
onApplySuggestion: (v: TokenizedSuggestionsItem<TokenValueBase>) => void;
};

Expand All @@ -19,6 +25,7 @@ export function SuggestionsList({
isLoading,
suggestions,
fullWidth,
size = 'm',
onApplySuggestion,
}: SuggestionsListProps) {
const {items, options, currentWord} = suggestions;
Expand Down Expand Up @@ -48,6 +55,7 @@ export function SuggestionsList({
empty: isEmpty && showEmptyState,
loading: isLoading,
'full-width': fullWidth,
size,
})}
>
<List
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const useSuggestions = <T extends TokenValueBase>({
const focusInfo = useFocusContext<T>();
const options = useOptionsContext<T>();

const {tokens} = inputInfo.state;
const {tokens, size} = inputInfo.state;
const {onChangeToken, onApplyChanges} = inputInfo.callbacks;
const {focus} = focusInfo.state;
const {onFocus} = focusInfo.callbacks;
Expand Down Expand Up @@ -234,6 +234,7 @@ export const useSuggestions = <T extends TokenValueBase>({
popupWidth,
popupOffset,
fullWidthSuggestions,
size,
},
callbacks: {
onApplySuggestion,
Expand All @@ -248,6 +249,7 @@ export const useSuggestions = <T extends TokenValueBase>({
popupOffset,
popupWidth,
selected,
size,
suggestions,
],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const useWrapper = () => {
const focusInfo = useFocusContext();
const inputInfo = useInputContext();

const {tokens, fields, isEditable, isClearable, className, wrapperRef} = inputInfo.state;
const {tokens, fields, isEditable, isClearable, className, size, wrapperRef} = inputInfo.state;
const {onApplyChanges, onClearInput} = inputInfo.callbacks;

const {focus} = focusInfo.state;
Expand All @@ -33,10 +33,18 @@ export const useWrapper = () => {

const classNames = React.useMemo(
() => ({
wrapper: b('wrapper', {disabled: !isEditable, focused: Boolean(focus)}, className),
wrapper: b(
'wrapper',
{
disabled: !isEditable,
focused: Boolean(focus),
size,
},
className,
),
clearButton: b('clear-button'),
}),
[className, focus, isEditable],
[className, focus, isEditable, size],
);

return React.useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function TokenizedInputContextProvider<T extends TokenValueBase>({
fields,
placeholder,
className,
size = 'm',
isEditable,
isClearable,
onKeyDown,
Expand All @@ -62,6 +63,7 @@ export function TokenizedInputContextProvider<T extends TokenValueBase>({
fields,
placeholder,
className,
size,
isEditable,
isClearable,
onChange,
Expand Down
Loading
Loading