diff --git a/src/components/CookieConsent/components/ConsentPopup/types.ts b/src/components/CookieConsent/components/ConsentPopup/types.ts
index 5183a0a3..dffdca3e 100644
--- a/src/components/CookieConsent/components/ConsentPopup/types.ts
+++ b/src/components/CookieConsent/components/ConsentPopup/types.ts
@@ -41,8 +41,6 @@ export interface ConsentPopupData {
noSubtitle?: boolean;
cookieList?: ConsentPopupCookieListItem[];
onClose: () => void;
- /* Disable modal animation */
- disableHeightTransition?: boolean;
}
export type ConsentPopupProps = ConsentPopupData &
diff --git a/src/components/Gallery/components/MobileGalleryActions/MobileGalleryActions.scss b/src/components/Gallery/components/MobileGalleryActions/MobileGalleryActions.scss
index f71f0e56..ff06f493 100644
--- a/src/components/Gallery/components/MobileGalleryActions/MobileGalleryActions.scss
+++ b/src/components/Gallery/components/MobileGalleryActions/MobileGalleryActions.scss
@@ -5,8 +5,19 @@ $block: '.#{variables.$ns}mobile-gallery-actions';
#{$block} {
--g-sheet-content-padding: var(--g-spacing-1) 0;
--g-button-border-radius: 0;
+ --g-list-item-view-border-radius: 0;
+ --g-list-item-view-padding-inline: 0;
+ --g-list-item-view-padding-block: 0;
+
+ &__cell {
+ width: 100%;
+ min-width: 0;
+ }
&__list-item {
+ --g-button-background-color-hover: transparent;
+ --g-button-focus-outline-offset: -2px;
+
justify-content: flex-start;
transition: background-color 0.2s ease-in-out;
}
diff --git a/src/components/Gallery/components/MobileGalleryActions/MobileGalleryActions.tsx b/src/components/Gallery/components/MobileGalleryActions/MobileGalleryActions.tsx
index 3abd1f71..684478f6 100644
--- a/src/components/Gallery/components/MobileGalleryActions/MobileGalleryActions.tsx
+++ b/src/components/Gallery/components/MobileGalleryActions/MobileGalleryActions.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
-import {Button, ButtonProps, Flex, List, Sheet} from '@gravity-ui/uikit';
+import {Button, ButtonProps, Flex, List, ListProps, Sheet} from '@gravity-ui/uikit';
import {block} from '../../../utils/cn';
import type {GalleryItemAction, GalleryItemProps} from '../../GalleryItem';
@@ -19,6 +19,11 @@ export type MobileGalleryActionsProps = {
export const MobileGalleryActions = ({open, actions = [], onClose}: MobileGalleryActionsProps) => {
const {t} = i18n.useTranslation();
+ const getItemTextValue = React.useCallback(
+ (item: GalleryItemAction) => (item.__titleT ? item.__titleT({t}) : item.title),
+ [t],
+ );
+
const renderListItem = React.useCallback(
(item: GalleryItemAction) => {
const title = item.__titleT ? item.__titleT({t}) : item.title;
@@ -35,7 +40,7 @@ export const MobileGalleryActions = ({open, actions = [], onClose}: MobileGaller
children: (
{item.icon}
@@ -67,15 +72,34 @@ export const MobileGalleryActions = ({open, actions = [], onClose}: MobileGaller
[t],
);
+ const renderItem = React.useCallback['renderItem']>>(
+ (ctx, helpers) => (
+
+
+ {renderListItem(ctx.item)}
+
+
+ ),
+ [renderListItem],
+ );
+
+ const handleOpenChange = React.useCallback(
+ (nextOpen: boolean) => {
+ if (!nextOpen) onClose();
+ },
+ [onClose],
+ );
+
return (
-
+
);
diff --git a/src/components/Gallery/i18n/en.json b/src/components/Gallery/i18n/en.json
index 3f065752..83b9222d 100644
--- a/src/components/Gallery/i18n/en.json
+++ b/src/components/Gallery/i18n/en.json
@@ -5,5 +5,6 @@
"next": "Next",
"back": "Back",
"copy-link": "Copy link",
- "download": "Download"
+ "download": "Download",
+ "actions": "Actions"
}
diff --git a/src/components/Gallery/i18n/ru.json b/src/components/Gallery/i18n/ru.json
index 9116bdd2..45cde68a 100644
--- a/src/components/Gallery/i18n/ru.json
+++ b/src/components/Gallery/i18n/ru.json
@@ -5,5 +5,6 @@
"next": "Следующий",
"back": "Назад",
"copy-link": "Скопировать ссылку",
- "download": "Скачать"
+ "download": "Скачать",
+ "actions": "Действия"
}
diff --git a/src/components/ItemSelector/ItemSelector.scss b/src/components/ItemSelector/ItemSelector.scss
index 7c8590b4..fd40aab8 100644
--- a/src/components/ItemSelector/ItemSelector.scss
+++ b/src/components/ItemSelector/ItemSelector.scss
@@ -4,7 +4,9 @@
$block: '.#{variables.$ns}item-selector';
#{$block} {
- --g-list-item-padding: 0 16px;
+ --g-list-item-view-padding-inline: 16px;
+ --g-list-item-view-padding-block: 0;
+ --g-list-item-view-border-radius: 0;
display: flex;
width: 100%;
@@ -24,7 +26,7 @@ $block: '.#{variables.$ns}item-selector';
justify-content: space-between;
align-items: center;
margin-block-end: 8px;
- padding: var(--g-list-item-padding);
+ padding: 0 16px;
min-height: 24px;
}
@@ -32,21 +34,38 @@ $block: '.#{variables.$ns}item-selector';
@include mixins.text-accent;
}
- &__item {
- display: flex;
- align-items: center;
+ &__filter {
+ box-sizing: border-box;
+ padding-inline: 16px;
+ margin-block-end: 4px;
+ }
+
+ &__items {
+ height: 196px;
+ overflow: auto;
+ }
+
+ &__list-item [role='gridcell'] {
+ display: block;
width: 100%;
- height: 100%;
+ min-width: 0;
+ }
- &_active {
- #{$block}__item-select {
- display: block;
+ // Without hover (touch) the actions are always visible
+ @media (hover: hover) {
+ &__list-item:not(:hover) {
+ #{$block}__item:not(#{$block}__item_active) #{$block}__item-select,
+ #{$block}__value-item:not(#{$block}__value-item_active) #{$block}__value-item-remove {
+ display: none;
}
}
}
- &__item-select {
- display: none;
+ &__item {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: 100%;
}
&__item-text {
@@ -62,12 +81,6 @@ $block: '.#{variables.$ns}item-selector';
align-items: center;
width: 100%;
overflow: hidden;
-
- &_active {
- #{$block}__value-item-remove {
- display: block;
- }
- }
}
&__value-item-text {
@@ -77,12 +90,4 @@ $block: '.#{variables.$ns}item-selector';
white-space: nowrap;
min-width: 0;
}
-
- &__value-item-remove {
- display: none;
- }
-}
-
-#{$block}__list-item {
- --_--item-padding: var(--g-list-item-padding, 0 16px);
}
diff --git a/src/components/ItemSelector/ItemSelector.tsx b/src/components/ItemSelector/ItemSelector.tsx
index 8439d9bc..49f26d76 100644
--- a/src/components/ItemSelector/ItemSelector.tsx
+++ b/src/components/ItemSelector/ItemSelector.tsx
@@ -1,7 +1,9 @@
import * as React from 'react';
import {Xmark} from '@gravity-ui/icons';
-import {Button, Icon, List, ListProps} from '@gravity-ui/uikit';
+import {Button, Icon, List, TextInput, useListFocusOwner} from '@gravity-ui/uikit';
+import {ListHelloPangeaDnd} from '@gravity-ui/uikit/hello-pangea-dnd';
+import {ListVirtualizer} from '@gravity-ui/uikit/virtualizer';
import {block} from '../utils/cn';
@@ -27,9 +29,100 @@ export interface ItemSelectorProps {
getItemId: (item: T) => string;
renderItemValue?: (item: T) => React.ReactNode;
- renderItem?: ListProps['renderItem'];
- renderValueItem?: ListProps['renderItem'];
- filterItem?: ListProps['filterItem'];
+ renderItem?: (item: T, active: boolean, index: number) => React.ReactNode;
+ renderValueItem?: ItemSelectorProps['renderItem'];
+ filterItem?: (filter: string) => (item: T) => boolean;
+}
+
+function ItemSelectorList({
+ items,
+ getItemId,
+ renderItem,
+ filterItem,
+ label,
+ onItemsUpdate,
+}: {
+ items: T[];
+ getItemId: ItemSelectorProps['getItemId'];
+ renderItem: NonNullable['renderItem']>;
+ filterItem: NonNullable['filterItem']>;
+ label: string;
+ onItemsUpdate?: (items: T[]) => void;
+}) {
+ const {t} = i18n.useTranslation();
+ const [filter, setFilter] = React.useState('');
+ const [activeItemId, setActiveItemId] = React.useState(null);
+ const focusOwner = useListFocusOwner();
+ const {onKeyDown, ...inputProps} = focusOwner.getInputProps();
+ const filteredItems = filter ? items.filter(filterItem(filter)) : items;
+ const list = (
+ {
+ // In List v8 `active` stays on the last hovered row; hover is handled by CSS
+ const active = ctx.state.active && Boolean(ctx.state.cursorVisible);
+ return onItemsUpdate ? (
+
+ {renderItem(ctx.item, active, ctx.index)}
+
+ ) : (
+
+
+ {renderItem(ctx.item, active, ctx.index)}
+
+
+ );
+ }}
+ />
+ );
+
+ return (
+
+ {
+ setFilter(query);
+ const firstItem = items.find(filterItem(query));
+ setActiveItemId(firstItem === undefined ? null : getItemId(firstItem));
+ }}
+ hasClear
+ />
+
+ {onItemsUpdate ? (
+ Boolean(filter)}
+ >
+ {list}
+
+ ) : (
+ list
+ )}
+
+
+ );
}
export class ItemSelector extends React.Component> {
@@ -57,7 +150,10 @@ export class ItemSelector extends React.Component> {
view="flat-secondary"
size="s"
className={b('item-select')}
- onClick={this.onAddItem.bind(this, item)}
+ onClick={(event) => {
+ event.stopPropagation();
+ this.onAddItem(item);
+ }}
>
{({t}) => t('button_select')}
@@ -80,7 +176,11 @@ export class ItemSelector extends React.Component> {
view="flat-secondary"
size="s"
className={b('value-item-remove')}
- onClick={() => this.onRemoveItem(item)}
+ aria-label={typeof title === 'string' ? title : this.props.getItemId(item)}
+ onClick={(event) => {
+ event.stopPropagation();
+ this.onRemoveItem(item);
+ }}
>
@@ -128,11 +228,8 @@ export class ItemSelector extends React.Component> {
const value = items.map(getItemId);
this.onUpdate(value);
};
- onMoveItem = ({oldIndex, newIndex}: {oldIndex: number; newIndex: number}) => {
- if (oldIndex !== newIndex) {
- const value = this.props.value.slice();
- this.onUpdate(List.moveListElement(value, oldIndex, newIndex));
- }
+ onMoveItems = (items: T[]) => {
+ this.onUpdate(items.map(this.props.getItemId));
};
onUpdate = (value: string[]) => {
this.props.onUpdate(value);
@@ -146,6 +243,7 @@ export class ItemSelector extends React.Component> {
renderValueItem = this.renderValueItem,
filterItem = this.filterItem,
hideSelectAllButton,
+ getItemId,
} = this.props;
const [items, selected] = this.getActualItems();
return (
@@ -166,13 +264,12 @@ export class ItemSelector extends React.Component> {
)}