Skip to content

Commit 7bf91c6

Browse files
Menu: improve types (#30908)
1 parent 1e78858 commit 7bf91c6

File tree

5 files changed

+64
-44
lines changed

5 files changed

+64
-44
lines changed

packages/devextreme/js/__internal/ui/chat/messagelist.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type dxContextMenu from '@js/ui/context_menu';
1919
import type { WidgetOptions } from '@js/ui/widget/ui.widget';
2020
import type { OptionChanged } from '@ts/core/widget/types';
2121
import Widget from '@ts/core/widget/widget';
22+
import type { ClickableCollectionWidgetItem } from '@ts/ui/collection/item';
2223
import ContextMenu from '@ts/ui/context_menu/context_menu';
2324
import type {
2425
ScrollView as ScrollViewType,
@@ -283,13 +284,14 @@ class MessageList extends Widget<Properties> {
283284
const editText = messageLocalization.format('dxChat-editingEditMessage');
284285
const deleteText = messageLocalization.format('dxChat-editingDeleteMessage');
285286

286-
const buttons: ContextMenuItem[] = [];
287+
const buttons: ClickableCollectionWidgetItem<ContextMenuItem>[] = [];
287288

288289
if (allowUpdating(message) && message.type !== 'image') {
289290
buttons.push({
290291
icon: 'edit',
291292
text: editText,
292293
disabled: isEditActionDisabled(message),
294+
// @ts-expect-error itemElement
293295
onClick: (e: ItemClick): void => {
294296
const onMessageEditStarted = onMessageEditingStart?.({
295297
event: e.event, message: message as TextMessage,
@@ -309,6 +311,7 @@ class MessageList extends Widget<Properties> {
309311
buttons.push({
310312
icon: 'trash',
311313
text: deleteText,
314+
// @ts-expect-error itemElement
312315
onClick(e: ItemClick): void {
313316
onMessageDeleting?.({ event: e.event, message });
314317
},

packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import type { ActionConfig } from '@ts/core/widget/component';
3737
import type { OptionChanged } from '@ts/core/widget/types';
3838
import type { SupportedKeys, WidgetProperties } from '@ts/core/widget/widget';
3939
import Widget from '@ts/core/widget/widget';
40+
import type { ClickableCollectionWidgetItem, ItemClickEvent } from '@ts/ui/collection/item';
4041
import type CollectionItem from '@ts/ui/collection/item';
4142
import CollectionWidgetItem from '@ts/ui/collection/item';
4243

@@ -117,8 +118,7 @@ export type Constructor<T> = new (...args: unknown[]) => T;
117118
export interface CollectionWidgetBaseProperties<
118119
// eslint-disable-next-line @typescript-eslint/no-explicit-any
119120
TComponent extends CollectionWidget<any, TItem, TKey> | any,
120-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
121-
TItem extends ItemLike = any,
121+
TItem extends ItemLike = CollectionWidgetItemProperties,
122122
// eslint-disable-next-line @typescript-eslint/no-explicit-any
123123
TKey extends CollectionItemKey = any,
124124
> extends CollectionWidgetOptions<TComponent, TItem, TKey>, Omit<
@@ -145,8 +145,7 @@ export interface CollectionWidgetBaseProperties<
145145
class CollectionWidget<
146146
// eslint-disable-next-line @typescript-eslint/no-explicit-any
147147
TProperties extends CollectionWidgetBaseProperties<any, TItem, TKey>,
148-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
149-
TItem extends ItemLike = any,
148+
TItem extends ItemLike = CollectionWidgetItemProperties,
150149
// eslint-disable-next-line @typescript-eslint/no-explicit-any
151150
TKey extends CollectionItemKey = any,
152151
> extends Widget<TProperties> {
@@ -231,12 +230,10 @@ class CollectionWidget<
231230
}
232231

233232
const itemData = this._getItemData($itemElement);
234-
// @ts-expect-error ts-error
235-
if (itemData?.onClick) {
233+
if (CollectionWidgetItem.isClickableItem(itemData)) {
236234
const actionArgs: ActionArgs<TItem> = {
237235
event: e,
238236
};
239-
// @ts-expect-error
240237
this._itemEventHandlerByHandler($itemElement, itemData.onClick, actionArgs);
241238
}
242239
// @ts-expect-error ts-error
@@ -1240,8 +1237,7 @@ class CollectionWidget<
12401237
}
12411238

12421239
_attachItemClickEvent(itemData: TItem, $itemElement: dxElementWrapper): void {
1243-
// @ts-expect-error ts-error
1244-
if (!itemData || !itemData.onClick) {
1240+
if (!itemData || !CollectionWidgetItem.isClickableItem(itemData)) {
12451241
return;
12461242
}
12471243

@@ -1252,7 +1248,6 @@ class CollectionWidget<
12521248
const actionArgs = {
12531249
event: e,
12541250
};
1255-
// @ts-expect-error ts-error
12561251
this._itemEventHandlerByHandler($itemElement, itemData.onClick, actionArgs);
12571252
},
12581253
);
@@ -1487,7 +1482,7 @@ class CollectionWidget<
14871482

14881483
_itemEventHandlerByHandler(
14891484
initiator: dxElementWrapper | Element,
1490-
handler: () => void,
1485+
handler: (e: ItemClickEvent<ClickableCollectionWidgetItem>) => void,
14911486
actionArgs: ActionArgs<TItem>,
14921487
actionConfig?: ActionConfig,
14931488
): void {

packages/devextreme/js/__internal/ui/collection/item.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import type { ItemInfo, NativeEventInfo } from '@js/common/core/events';
12
import type { dxElementWrapper } from '@js/core/renderer';
23
import $ from '@js/core/renderer';
34
import { each } from '@js/core/utils/iterator';
45
import { attachInstanceToElement, getInstanceByElement } from '@js/core/utils/public_component';
5-
import type { CollectionWidgetItem } from '@js/ui/collection/ui.collection_widget.base';
6+
import { isObject } from '@js/core/utils/type';
7+
import type { CollectionWidgetItem, ItemLike } from '@js/ui/collection/ui.collection_widget.base';
68

79
const INVISIBLE_STATE_CLASS = 'dx-state-invisible';
810
const DISABLED_STATE_CLASS = 'dx-state-disabled';
@@ -51,6 +53,17 @@ export interface ItemExtraOption<TProperties> {
5153
) => () => void;
5254
}
5355

56+
export type ItemClickEvent<TProperties> =
57+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
58+
NativeEventInfo<any, KeyboardEvent | MouseEvent | PointerEvent>
59+
& ItemInfo<TProperties>;
60+
61+
export type ClickableCollectionWidgetItem<
62+
TProperties extends CollectionWidgetItem = CollectionWidgetItem,
63+
> = TProperties & {
64+
onClick: (e: ItemClickEvent<TProperties>) => void;
65+
};
66+
5467
class CollectionItem<
5568
TProperties extends CollectionWidgetItem = CollectionWidgetItem,
5669
> {
@@ -159,9 +172,14 @@ class CollectionItem<
159172

160173
// eslint-disable-next-line @typescript-eslint/no-explicit-any
161174
static getInstance<T = CollectionItem<any>>($element: dxElementWrapper): T {
162-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
163175
return getInstanceByElement($element, this);
164176
}
177+
178+
static isClickableItem(
179+
item: ItemLike,
180+
): item is ClickableCollectionWidgetItem {
181+
return isObject(item) && 'onClick' in item;
182+
}
165183
}
166184

167185
export default CollectionItem;

packages/devextreme/js/__internal/ui/context_menu/context_menu.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,12 @@ import type {
2929
HiddenEvent,
3030
HidingEvent,
3131
Item,
32-
ItemClickEvent,
3332
PositioningEvent,
3433
Properties,
3534
ShowingEvent,
3635
ShownEvent,
3736
} from '@js/ui/context_menu';
38-
import type dxContextMenu from '@js/ui/context_menu';
37+
import type dxMenuBase from '@js/ui/context_menu/ui.menu_base';
3938
import type {
4039
dxMenuBaseItem,
4140
SubmenuHiddenEvent,
@@ -45,10 +44,11 @@ import type {
4544
} from '@js/ui/menu';
4645
import type { Properties as OverlayProperties } from '@js/ui/overlay';
4746
import { current as currentTheme, isGeneric } from '@js/ui/themes';
48-
import type { ActionArguments } from '@ts/core/m_action';
4947
import type { OptionChanged } from '@ts/core/widget/types';
5048
import type { SupportedKeys } from '@ts/core/widget/widget';
51-
import type { ClickEvent, HoverEvent, MenuBaseProperties } from '@ts/ui/context_menu/menu_base';
49+
import type {
50+
ClickEvent, HoverEvent, ItemClickActionArguments, MenuBaseProperties,
51+
} from '@ts/ui/context_menu/menu_base';
5252
import MenuBase from '@ts/ui/context_menu/menu_base';
5353
import type { InternalNode } from '@ts/ui/hierarchical_collection/data_converter';
5454
import Overlay from '@ts/ui/overlay/overlay';
@@ -109,8 +109,6 @@ interface SubmenuCreatedEvent<TItem extends dxMenuBaseItem = dxMenuBaseItem> {
109109
submenuElement: Element;
110110
itemData: TItem;
111111
}
112-
type ItemClickActionArguments =
113-
ActionArguments<dxContextMenu<ContextMenuProperties>, ItemClickEvent>;
114112

115113
interface ContextMenuActions {
116114
onShowing?: ((e: ShowingEvent | SubmenuShowingEvent | ChatMenuShowingEvent) => void);
@@ -127,20 +125,18 @@ interface ContextMenuActions {
127125

128126
type ContextMenuPropertiesKeys = Exclude<keyof Properties, keyof MenuBaseProperties>;
129127

130-
export interface ContextMenuProperties<
131-
TItem extends dxMenuBaseItem = Item,
132-
> extends
128+
export interface ContextMenuProperties<TItem extends Item = Item> extends
133129
MenuBaseProperties<TItem>,
134130
Pick<Properties, ContextMenuPropertiesKeys> {
135131
hideOnParentScroll?: boolean;
136132
visualContainer?: string | Element | Window | null;
137133
overlayContainer?: string | Element | null;
138134
boundaryOffset?: PositionConfig['boundaryOffset'];
139-
onSubmenuCreated?: ((e) => void) | null;
140-
onLeftFirstItem?: ((e) => void) | null;
141-
onLeftLastItem?: ((e) => void) | null;
142-
onCloseRootSubmenu?: ((e) => void) | null;
143-
onExpandLastSubmenu?: ((e) => void) | null;
135+
onSubmenuCreated?: ((e: SubmenuCreatedEvent) => void) | null;
136+
onLeftFirstItem?: (($item?: dxElementWrapper) => void) | null;
137+
onLeftLastItem?: (($item?: dxElementWrapper) => void) | null;
138+
onCloseRootSubmenu?: (($item?: dxElementWrapper) => void) | null;
139+
onExpandLastSubmenu?: (($item?: dxElementWrapper) => void) | null;
144140
}
145141

146142
class ContextMenu<
@@ -973,8 +969,9 @@ class ContextMenu<
973969
}
974970

975971
// TODO: try to simplify it
976-
// @ts-expect-error ts-error
977-
_updateSubmenuVisibilityOnClick(actionArgs: ItemClickActionArguments): void {
972+
_updateSubmenuVisibilityOnClick(
973+
actionArgs: ItemClickActionArguments<dxMenuBase<ContextMenuProperties>, Item>,
974+
): void {
978975
if (!actionArgs.args?.length) {
979976
return;
980977
}
@@ -1005,7 +1002,6 @@ class ContextMenu<
10051002
return;
10061003
}
10071004

1008-
// @ts-expect-error ts-error
10091005
this._updateSelectedItemOnClick(actionArgs);
10101006

10111007
// T238943. Give the workaround with e.cancel and remove this hack

packages/devextreme/js/__internal/ui/context_menu/menu_base.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,22 @@ const DX_ICON_WITH_URL_CLASS = 'dx-icon-with-url';
4646
const ITEM_URL_CLASS = 'dx-item-url';
4747
const DX_MENU_ITEM_DATA_KEY = 'dxMenuItemDataKey';
4848

49-
type ItemClickEvent =
50-
NativeEventInfo<dxMenuBase<MenuBaseProperties>, MouseEvent | PointerEvent | TouchEvent>
51-
& ItemInfo<dxMenuBaseItem>;
49+
type ItemClickEvent<TComponent, TItem> =
50+
NativeEventInfo<TComponent, MouseEvent | PointerEvent | TouchEvent>
51+
& ItemInfo<TItem>;
5252
export type HoverEvent = DxEvent<MouseEvent | PointerEvent>;
5353
export type ClickEvent = DxEvent<MouseEvent | PointerEvent | TouchEvent>;
54-
export type ItemClickActionArguments = ActionArguments<
55-
dxMenuBase<MenuBaseProperties>,
56-
ItemClickEvent
54+
export type ItemClickActionArguments<
55+
TComponent extends dxMenuBase<MenuBaseProperties> = dxMenuBase<MenuBaseProperties>,
56+
TItem extends dxMenuBaseItem = dxMenuBaseItem,
57+
> = ActionArguments<
58+
TComponent,
59+
ItemClickEvent<TComponent, TItem>
5760
>;
5861
type MenuBaseNode = InternalNode & dxMenuBaseItem;
5962

6063
export interface MenuBaseProperties<
61-
TItem extends dxMenuBaseItem = Item,
64+
TItem extends dxMenuBaseItem = dxMenuBaseItem,
6265
// @ts-expect-error ts-error
6366
> extends dxMenuBaseOptions<MenuBase, TItem> {
6467
focusedElement?: Element | null;
@@ -272,7 +275,7 @@ class MenuBase<
272275
return;
273276
}
274277

275-
const node: Item | null = this._dataAdapter.getNodeByKey(selectedKey);
278+
const node: MenuBaseNode | null = this._dataAdapter.getNodeByKey(selectedKey);
276279

277280
if (!node || node.selectable === false) {
278281
return;
@@ -412,9 +415,8 @@ class MenuBase<
412415
return delay;
413416
}
414417

415-
// TODO: try to simplify
416418
_getItemElementByEventArgs(
417-
eventArgs: HoverEvent | ClickEvent,
419+
eventArgs: DxEvent,
418420
): dxElementWrapper | null {
419421
let $target = $(eventArgs.target);
420422

@@ -631,6 +633,10 @@ class MenuBase<
631633
e._skipHandling = true;
632634
}
633635

636+
_isUrlItem(item: Item | dxMenuBaseItem | undefined): item is Item {
637+
return !!item && 'url' in item && !!item.url;
638+
}
639+
634640
_itemClick(actionArgs: ItemClickActionArguments): void {
635641
const { event, itemData } = actionArgs.args?.[0] ?? {};
636642

@@ -641,7 +647,7 @@ class MenuBase<
641647
const $itemElement = this._getItemElementByEventArgs(event);
642648
const link = $itemElement?.find(`.${ITEM_URL_CLASS}`)[0];
643649

644-
if (!itemData?.url || !link) {
650+
if (!this._isUrlItem(itemData) || !link) {
645651
return;
646652
}
647653

@@ -667,7 +673,7 @@ class MenuBase<
667673
}
668674

669675
_updateSelectedItemOnClick(actionArgs: ItemClickActionArguments): void {
670-
const args: ItemClickEvent = actionArgs.args ? actionArgs.args[0] : actionArgs;
676+
const args = actionArgs.args ? actionArgs.args[0] : actionArgs;
671677

672678
const { itemData } = args;
673679

@@ -793,7 +799,9 @@ class MenuBase<
793799
}
794800

795801
selectItem(itemElement: Element | dxMenuBaseItem): void {
796-
const itemData = itemElement.nodeType ? this._getItemData(itemElement as Element) : itemElement;
802+
const isElement = (item: Element | dxMenuBaseItem): item is Element => typeof item === 'object' && 'nodeType' in item && !!item.nodeType;
803+
804+
const itemData = isElement(itemElement) ? this._getItemData(itemElement) : itemElement;
797805
const selectedKey = this._dataAdapter.getSelectedNodesKeys()[0];
798806
const selectedItem = this.option('selectedItem');
799807
const node = this._dataAdapter.getNodeByItem(itemData);

0 commit comments

Comments
 (0)