Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(input, input-number, input-text, input-date-picker, input-time-picker, filter, menu-item): ignore canceled events and enforce cancellations where needed #8952

Merged
merged 7 commits into from
Apr 13, 2024
96 changes: 96 additions & 0 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,80 +8,176 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { Alignment, Appearance, Columns, FlipContext, Kind, Layout, LogicalFlowPosition, Position, Scale, SelectionMode, Status, Width } from "./components/interfaces";
import { RequestedItem } from "./components/accordion/interfaces";
import { RequestedItem as RequestedItem1 } from "./components/accordion-item/interfaces";
import { ActionMessages } from "./components/action/assets/action/t9n";
import { EffectivePlacement, LogicalPlacement, MenuPlacement, OverlayPositioning, ReferenceElement } from "./utils/floating-ui";
import { ActionBarMessages } from "./components/action-bar/assets/action-bar/t9n";
import { ActionGroupMessages } from "./components/action-group/assets/action-group/t9n";
import { ActionPadMessages } from "./components/action-pad/assets/action-pad/t9n";
import { AlertDuration, Sync } from "./components/alert/interfaces";
import { NumberingSystem } from "./utils/locale";
import { AlertMessages } from "./components/alert/assets/alert/t9n";
import { HeadingLevel } from "./components/functional/Heading";
import { BlockMessages } from "./components/block/assets/block/t9n";
import { BlockSectionToggleDisplay } from "./components/block-section/interfaces";
import { BlockSectionMessages } from "./components/block-section/assets/block-section/t9n";
import { ButtonAlignment, DropdownIconType } from "./components/button/interfaces";
import { ButtonMessages } from "./components/button/assets/button/t9n";
import { CardMessages } from "./components/card/assets/card/t9n";
import { ChipMessages } from "./components/chip/assets/chip/t9n";
import { ColorValue, InternalColor } from "./components/color-picker/interfaces";
import { Format } from "./components/color-picker/utils";
import { ColorPickerMessages } from "./components/color-picker/assets/color-picker/t9n";
import { ComboboxChildElement, SelectionDisplay } from "./components/combobox/interfaces";
import { ComboboxMessages } from "./components/combobox/assets/combobox/t9n";
import { DatePickerMessages } from "./components/date-picker/assets/date-picker/t9n";
import { DateLocaleData } from "./components/date-picker/utils";
import { HoverRange } from "./utils/date";
import { RequestedItem as RequestedItem2 } from "./components/dropdown-group/interfaces";
import { ItemKeyboardEvent } from "./components/dropdown/interfaces";
import { FilterMessages } from "./components/filter/assets/filter/t9n";
import { FlowItemLikeElement } from "./components/flow/interfaces";
import { FlowItemMessages } from "./components/flow-item/assets/flow-item/t9n";
import { ColorStop, DataSeries } from "./components/graph/interfaces";
import { HandleMessages } from "./components/handle/assets/handle/t9n";
import { HandleChange, HandleNudge } from "./components/handle/interfaces";
import { InlineEditableMessages } from "./components/inline-editable/assets/inline-editable/t9n";
import { InputPlacement } from "./components/input/interfaces";
import { InputMessages } from "./components/input/assets/input/t9n";
import { InputDatePickerMessages } from "./components/input-date-picker/assets/input-date-picker/t9n";
import { InputNumberMessages } from "./components/input-number/assets/input-number/t9n";
import { InputTextMessages } from "./components/input-text/assets/input-text/t9n";
import { InputTimePickerMessages } from "./components/input-time-picker/assets/input-time-picker/t9n";
import { TimePickerMessages } from "./components/time-picker/assets/time-picker/t9n";
import { InputTimeZoneMessages } from "./components/input-time-zone/assets/input-time-zone/t9n";
import { TimeZoneMode } from "./components/input-time-zone/interfaces";
import { ListDragDetail } from "./components/list/interfaces";
import { ItemData } from "./components/list-item/interfaces";
import { ListMessages } from "./components/list/assets/list/t9n";
import { SelectionAppearance } from "./components/list/resources";
import { ListItemMessages } from "./components/list-item/assets/list-item/t9n";
import { MenuMessages } from "./components/menu/assets/menu/t9n";
import { MenuItemMessages } from "./components/menu-item/assets/menu-item/t9n";
import { MenuItemCustomEvent } from "./components/menu-item/interfaces";
import { MeterLabelType } from "./components/meter/interfaces";
import { ModalMessages } from "./components/modal/assets/modal/t9n";
import { NoticeMessages } from "./components/notice/assets/notice/t9n";
import { PaginationMessages } from "./components/pagination/assets/pagination/t9n";
import { PanelMessages } from "./components/panel/assets/panel/t9n";
import { ItemData as ItemData1, ListFocusId } from "./components/pick-list/shared-list-logic";
import { ICON_TYPES } from "./components/pick-list/resources";
import { PickListItemMessages } from "./components/pick-list-item/assets/pick-list-item/t9n";
import { PopoverMessages } from "./components/popover/assets/popover/t9n";
import { RatingMessages } from "./components/rating/assets/rating/t9n";
import { ScrimMessages } from "./components/scrim/assets/scrim/t9n";
import { DisplayMode } from "./components/sheet/interfaces";
import { DisplayMode as DisplayMode1 } from "./components/shell-panel/interfaces";
import { ShellPanelMessages } from "./components/shell-panel/assets/shell-panel/t9n";
import { DragDetail } from "./utils/sortableComponent";
import { StepperItemChangeEventDetail, StepperItemEventDetail, StepperItemKeyEventDetail, StepperLayout } from "./components/stepper/interfaces";
import { StepperMessages } from "./components/stepper/assets/stepper/t9n";
import { StepperItemMessages } from "./components/stepper-item/assets/stepper-item/t9n";
import { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces";
import { TabNavMessages } from "./components/tab-nav/assets/tab-nav/t9n";
import { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
import { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
import { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
import { TableMessages } from "./components/table/assets/table/t9n";
import { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n";
import { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n";
import { TextAreaMessages } from "./components/text-area/assets/text-area/t9n";
import { TileSelectType } from "./components/tile-select/interfaces";
import { TileSelectGroupLayout } from "./components/tile-select-group/interfaces";
import { TipMessages } from "./components/tip/assets/tip/t9n";
import { TipManagerMessages } from "./components/tip-manager/assets/tip-manager/t9n";
import { TreeItemSelectDetail } from "./components/tree-item/interfaces";
import { ValueListMessages } from "./components/value-list/assets/value-list/t9n";
import { ListItemAndHandle } from "./components/value-list-item/interfaces";
export { Alignment, Appearance, Columns, FlipContext, Kind, Layout, LogicalFlowPosition, Position, Scale, SelectionMode, Status, Width } from "./components/interfaces";
export { RequestedItem } from "./components/accordion/interfaces";
export { RequestedItem as RequestedItem1 } from "./components/accordion-item/interfaces";
export { ActionMessages } from "./components/action/assets/action/t9n";
export { EffectivePlacement, LogicalPlacement, MenuPlacement, OverlayPositioning, ReferenceElement } from "./utils/floating-ui";
export { ActionBarMessages } from "./components/action-bar/assets/action-bar/t9n";
export { ActionGroupMessages } from "./components/action-group/assets/action-group/t9n";
export { ActionPadMessages } from "./components/action-pad/assets/action-pad/t9n";
export { AlertDuration, Sync } from "./components/alert/interfaces";
export { NumberingSystem } from "./utils/locale";
export { AlertMessages } from "./components/alert/assets/alert/t9n";
export { HeadingLevel } from "./components/functional/Heading";
export { BlockMessages } from "./components/block/assets/block/t9n";
export { BlockSectionToggleDisplay } from "./components/block-section/interfaces";
export { BlockSectionMessages } from "./components/block-section/assets/block-section/t9n";
export { ButtonAlignment, DropdownIconType } from "./components/button/interfaces";
export { ButtonMessages } from "./components/button/assets/button/t9n";
export { CardMessages } from "./components/card/assets/card/t9n";
export { ChipMessages } from "./components/chip/assets/chip/t9n";
export { ColorValue, InternalColor } from "./components/color-picker/interfaces";
export { Format } from "./components/color-picker/utils";
export { ColorPickerMessages } from "./components/color-picker/assets/color-picker/t9n";
export { ComboboxChildElement, SelectionDisplay } from "./components/combobox/interfaces";
export { ComboboxMessages } from "./components/combobox/assets/combobox/t9n";
export { DatePickerMessages } from "./components/date-picker/assets/date-picker/t9n";
export { DateLocaleData } from "./components/date-picker/utils";
export { HoverRange } from "./utils/date";
export { RequestedItem as RequestedItem2 } from "./components/dropdown-group/interfaces";
export { ItemKeyboardEvent } from "./components/dropdown/interfaces";
export { FilterMessages } from "./components/filter/assets/filter/t9n";
export { FlowItemLikeElement } from "./components/flow/interfaces";
export { FlowItemMessages } from "./components/flow-item/assets/flow-item/t9n";
export { ColorStop, DataSeries } from "./components/graph/interfaces";
export { HandleMessages } from "./components/handle/assets/handle/t9n";
export { HandleChange, HandleNudge } from "./components/handle/interfaces";
export { InlineEditableMessages } from "./components/inline-editable/assets/inline-editable/t9n";
export { InputPlacement } from "./components/input/interfaces";
export { InputMessages } from "./components/input/assets/input/t9n";
export { InputDatePickerMessages } from "./components/input-date-picker/assets/input-date-picker/t9n";
export { InputNumberMessages } from "./components/input-number/assets/input-number/t9n";
export { InputTextMessages } from "./components/input-text/assets/input-text/t9n";
export { InputTimePickerMessages } from "./components/input-time-picker/assets/input-time-picker/t9n";
export { TimePickerMessages } from "./components/time-picker/assets/time-picker/t9n";
export { InputTimeZoneMessages } from "./components/input-time-zone/assets/input-time-zone/t9n";
export { TimeZoneMode } from "./components/input-time-zone/interfaces";
export { ListDragDetail } from "./components/list/interfaces";
export { ItemData } from "./components/list-item/interfaces";
export { ListMessages } from "./components/list/assets/list/t9n";
export { SelectionAppearance } from "./components/list/resources";
export { ListItemMessages } from "./components/list-item/assets/list-item/t9n";
export { MenuMessages } from "./components/menu/assets/menu/t9n";
export { MenuItemMessages } from "./components/menu-item/assets/menu-item/t9n";
export { MenuItemCustomEvent } from "./components/menu-item/interfaces";
export { MeterLabelType } from "./components/meter/interfaces";
export { ModalMessages } from "./components/modal/assets/modal/t9n";
export { NoticeMessages } from "./components/notice/assets/notice/t9n";
export { PaginationMessages } from "./components/pagination/assets/pagination/t9n";
export { PanelMessages } from "./components/panel/assets/panel/t9n";
export { ItemData as ItemData1, ListFocusId } from "./components/pick-list/shared-list-logic";
export { ICON_TYPES } from "./components/pick-list/resources";
export { PickListItemMessages } from "./components/pick-list-item/assets/pick-list-item/t9n";
export { PopoverMessages } from "./components/popover/assets/popover/t9n";
export { RatingMessages } from "./components/rating/assets/rating/t9n";
export { ScrimMessages } from "./components/scrim/assets/scrim/t9n";
export { DisplayMode } from "./components/sheet/interfaces";
export { DisplayMode as DisplayMode1 } from "./components/shell-panel/interfaces";
export { ShellPanelMessages } from "./components/shell-panel/assets/shell-panel/t9n";
export { DragDetail } from "./utils/sortableComponent";
export { StepperItemChangeEventDetail, StepperItemEventDetail, StepperItemKeyEventDetail, StepperLayout } from "./components/stepper/interfaces";
export { StepperMessages } from "./components/stepper/assets/stepper/t9n";
export { StepperItemMessages } from "./components/stepper-item/assets/stepper-item/t9n";
export { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces";
export { TabNavMessages } from "./components/tab-nav/assets/tab-nav/t9n";
export { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
export { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
export { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
export { TableMessages } from "./components/table/assets/table/t9n";
export { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n";
export { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n";
export { TextAreaMessages } from "./components/text-area/assets/text-area/t9n";
export { TileSelectType } from "./components/tile-select/interfaces";
export { TileSelectGroupLayout } from "./components/tile-select-group/interfaces";
export { TipMessages } from "./components/tip/assets/tip/t9n";
export { TipManagerMessages } from "./components/tip-manager/assets/tip-manager/t9n";
export { TreeItemSelectDetail } from "./components/tree-item/interfaces";
export { ValueListMessages } from "./components/value-list/assets/value-list/t9n";
export { ListItemAndHandle } from "./components/value-list-item/interfaces";
export namespace Components {
interface CalciteAccordion {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@ export class ActionBar
};

handleTooltipSlotChange = (event: Event): void => {
const tooltips = slotChangeGetAssignedElements(event).filter(
(el) => el?.matches("calcite-tooltip"),
const tooltips = slotChangeGetAssignedElements(event).filter((el) =>
el?.matches("calcite-tooltip"),
) as HTMLCalciteTooltipElement[];

this.expandTooltip = tooltips[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,16 @@ export class ActionPad
}

handleDefaultSlotChange = (event: Event): void => {
const groups = slotChangeGetAssignedElements(event).filter(
(el) => el?.matches("calcite-action-group"),
const groups = slotChangeGetAssignedElements(event).filter((el) =>
el?.matches("calcite-action-group"),
) as HTMLCalciteActionGroupElement[];

this.setGroupLayout(groups);
};

handleTooltipSlotChange = (event: Event): void => {
const tooltips = slotChangeGetAssignedElements(event).filter(
(el) => el?.matches("calcite-tooltip"),
const tooltips = slotChangeGetAssignedElements(event).filter((el) =>
el?.matches("calcite-tooltip"),
) as HTMLCalciteTooltipElement[];

this.expandTooltip = tooltips[0];
Expand Down
4 changes: 4 additions & 0 deletions packages/calcite-components/src/components/filter/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ export class Filter
};

keyDownHandler = (event: KeyboardEvent): void => {
if (event.defaultPrevented) {
return;
}

if (event.key === "Escape") {
this.clear();
event.preventDefault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,7 @@ export class InputDatePicker
}

if (key === "Enter") {
event.preventDefault();
Copy link
Member

@driskull driskull Mar 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I'm noticing is that we're having to call event.preventDefault() for every key. This is fine, but maybe it would be good to have a dom utility function to handle this better for us.

Something like:

export function preventDefaultForKeys(event: KeyboardEvent, keys: KeyboardEvent["key"][]): void {
  if (!event.defaultPrevented && keys.includes(event.key)) {
    event.preventDefault();
  }
}

usage:

preventDefaultForKeys(["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"], [" ", "Enter"]);

what do you think @jcfranco?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed util would reduce explicit calls to the method, but does duplicate the keys.

Before

onKeyDown(event: KeyboardEvent): void {
  if (event.key === "ArrowDown" {
    /* ... */
    event.preventDefault();
  } else if (event.key === "ArrowLeft" {
    /* ... */
    event.preventDefault();
  }

}

After

onKeyDown(event: KeyboardEvent): void {
  preventDefaultForKeys(["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"], [" ", "Enter"]);

  if (event.key === "ArrowDown" {
    /* ... */
  } else if (event.key === "ArrowLeft" {
    /* ... */
  }
}

this.commitValue();

if (this.shouldFocusRangeEnd()) {
Expand All @@ -902,7 +903,6 @@ export class InputDatePicker
}

if (submitForm(this)) {
event.preventDefault();
this.restoreInputFocus();
}
} else if (key === "ArrowDown") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -542,14 +542,15 @@ export class InputNumber
//--------------------------------------------------------------------------

keyDownHandler = (event: KeyboardEvent): void => {
if (this.readOnly || this.disabled) {
if (this.readOnly || this.disabled || event.defaultPrevented) {
return;
}

if (this.isClearable && event.key === "Escape") {
this.clearInputValue(event);
event.preventDefault();
}
if (event.key === "Enter" && !event.defaultPrevented) {
if (event.key === "Enter") {
if (submitForm(this)) {
event.preventDefault();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,14 +415,15 @@ export class InputText
//--------------------------------------------------------------------------

keyDownHandler = (event: KeyboardEvent): void => {
if (this.readOnly || this.disabled) {
if (this.readOnly || this.disabled || event.defaultPrevented) {
return;
}

if (this.isClearable && event.key === "Escape") {
this.clearInputTextValue(event);
event.preventDefault();
}
if (event.key === "Enter" && !event.defaultPrevented) {
if (event.key === "Enter") {
if (submitForm(this)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the input text, we are only calling preventDefault if the input is inside a form. I wonder if we should call event.preventDefault() for the enter key all the time? cc @jcfranco. Is it inconsistent to only call this when inside a form?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it inconsistent to only call this when inside a form?

Not according to our conventions. We should only be canceling native events if the component reacts to an event for a specific interaction.

event.preventDefault();
}
Expand Down
5 changes: 3 additions & 2 deletions packages/calcite-components/src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -607,14 +607,15 @@ export class Input
//--------------------------------------------------------------------------

keyDownHandler = (event: KeyboardEvent): void => {
if (this.readOnly || this.disabled) {
if (this.readOnly || this.disabled || event.defaultPrevented) {
return;
}

if (this.isClearable && event.key === "Escape") {
this.clearInputValue(event);
event.preventDefault();
}
if (event.key === "Enter" && !event.defaultPrevented) {
if (event.key === "Enter") {
if (submitForm(this)) {
event.preventDefault();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ export class CalciteMenuItem implements LoadableComponent, T9nComponent, Localiz
const { hasSubmenu, href, layout, open, submenuItems } = this;
const key = event.key;
const targetIsDropdown = event.target === this.dropdownActionEl;

if (event.defaultPrevented) {
return;
}

if (key === " " || key === "Enter") {
if (hasSubmenu && (!href || (href && targetIsDropdown))) {
this.open = !open;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this example, shouldn't we be preventing default for enter and space key regardless?

Currently, it is only preventing default if the key is space, component has href and target is a dropdown.

if (key === " " || key === "Enter") {
      if (hasSubmenu && (!href || (href && targetIsDropdown))) {
        this.open = !open;
      }
      if (!(href && targetIsDropdown) && key !== "Enter") {
        this.selectMenuItem(event);
      }
      if (key === " " || (href && targetIsDropdown)) {
        event.preventDefault();
      }
    } 

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are if/else statement blocks that won't be executed (e.g., Enter + !hasSubmenu), so I wouldn't agree we should cancel regardless of key pressed.

Expand Down
4 changes: 2 additions & 2 deletions packages/calcite-components/src/components/panel/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ export class Panel
};

handleActionBarSlotChange = (event: Event): void => {
const actionBars = slotChangeGetAssignedElements(event).filter(
(el) => el?.matches("calcite-action-bar"),
const actionBars = slotChangeGetAssignedElements(event).filter((el) =>
el?.matches("calcite-action-bar"),
) as HTMLCalciteActionBarElement[];

actionBars.forEach((actionBar) => (actionBar.layout = "horizontal"));
Expand Down
4 changes: 2 additions & 2 deletions packages/calcite-components/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ export class Select

this.clearInternalSelect();

optionsAndGroups.forEach(
(optionOrGroup) => this.selectEl?.append(this.toNativeElement(optionOrGroup)),
optionsAndGroups.forEach((optionOrGroup) =>
this.selectEl?.append(this.toNativeElement(optionOrGroup)),
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,8 @@ export class ShellPanel implements ConditionalSlotComponent, LocalizedComponent,
};

handleActionBarSlotChange = (event: Event): void => {
const actionBars = slotChangeGetAssignedElements(event).filter(
(el) => el?.matches("calcite-action-bar"),
const actionBars = slotChangeGetAssignedElements(event).filter((el) =>
el?.matches("calcite-action-bar"),
) as HTMLCalciteActionBarElement[];

this.actionBars = actionBars;
Expand Down
5 changes: 3 additions & 2 deletions packages/calcite-components/src/components/table/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,9 @@ export class Table implements LocalizedComponent, LoadableComponent, T9nComponen
break;
}

const destinationCount = this.allRows?.find((row) => row.positionAll === rowPosition)
?.cellCount;
const destinationCount = this.allRows?.find(
(row) => row.positionAll === rowPosition,
)?.cellCount;

const adjustedPos = cellPosition > destinationCount ? destinationCount : cellPosition;

Expand Down
Loading