From adb0d93a31917fabc630eb3a6cacfe90e1ce1380 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Fri, 14 Oct 2022 15:05:54 -0700 Subject: [PATCH 01/17] fix: fix jarring positioning when a closed component is first opened --- src/components/popover/popover.stories.ts | 19 +++++++++++++++++++ src/utils/floating-ui.ts | 3 +++ 2 files changed, 22 insertions(+) diff --git a/src/components/popover/popover.stories.ts b/src/components/popover/popover.stories.ts index 083310498d2..6a2bf4653b0 100644 --- a/src/components/popover/popover.stories.ts +++ b/src/components/popover/popover.stories.ts @@ -101,3 +101,22 @@ export const flipPlacements_TestOnly = (): string => html` document.querySelector(".my-popover").flipPlacements = ["right"]; `; + +export const correctInitialPosition_TestOnly = (): string => html` + + + +

Floating

+
+
+ +`; diff --git a/src/utils/floating-ui.ts b/src/utils/floating-ui.ts index 8d304d24788..9e568c51aed 100644 --- a/src/utils/floating-ui.ts +++ b/src/utils/floating-ui.ts @@ -380,6 +380,9 @@ export function connectFloatingUI( disconnectFloatingUI(component, referenceEl, floatingEl); + // ensure position matches for initial positioning + // floatingEl.style.position = component.overlayPositioning; + cleanupMap.set( component, autoUpdate(referenceEl, floatingEl, () => { From 048e245202df53d997f6148e8ece9faacd84b8f0 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Fri, 14 Oct 2022 16:49:38 -0700 Subject: [PATCH 02/17] uncomment actual fix and switch story to be stepped --- src/components/popover/popover.stories.ts | 36 +++++++++++------------ src/utils/floating-ui.ts | 2 +- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/components/popover/popover.stories.ts b/src/components/popover/popover.stories.ts index 6a2bf4653b0..6c85a4bc07c 100644 --- a/src/components/popover/popover.stories.ts +++ b/src/components/popover/popover.stories.ts @@ -1,6 +1,6 @@ import { select, number, text } from "@storybook/addon-knobs"; import { html } from "../../../support/formatting"; -import { boolean, storyFilters } from "../../../.storybook/helpers"; +import { boolean, createSteps, stepStory, storyFilters } from "../../../.storybook/helpers"; import { placements } from "../../utils/floating-ui"; import readme from "./readme.md"; import { defaultPopoverPlacement } from "../popover/resources"; @@ -102,21 +102,19 @@ export const flipPlacements_TestOnly = (): string => html` `; -export const correctInitialPosition_TestOnly = (): string => html` - - - -

Floating

-
-
- -`; +export const correctInitialPosition_TestOnly = stepStory( + (): string => html` + + + +

Floating

+
+
+ `, + createSteps("calcite-popover").click("#popover").snapshot("correct initial position") +); diff --git a/src/utils/floating-ui.ts b/src/utils/floating-ui.ts index 9e568c51aed..c96e630908e 100644 --- a/src/utils/floating-ui.ts +++ b/src/utils/floating-ui.ts @@ -381,7 +381,7 @@ export function connectFloatingUI( disconnectFloatingUI(component, referenceEl, floatingEl); // ensure position matches for initial positioning - // floatingEl.style.position = component.overlayPositioning; + floatingEl.style.position = component.overlayPositioning; cleanupMap.set( component, From 23cedd02b679de2611e0a20c40369cb7b13c05a4 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Fri, 14 Oct 2022 17:36:49 -0700 Subject: [PATCH 03/17] tweak story to capture initial positioning --- src/components/popover/popover.stories.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/popover/popover.stories.ts b/src/components/popover/popover.stories.ts index 6c85a4bc07c..b3b3c095297 100644 --- a/src/components/popover/popover.stories.ts +++ b/src/components/popover/popover.stories.ts @@ -116,5 +116,15 @@ export const correctInitialPosition_TestOnly = stepStory( `, - createSteps("calcite-popover").click("#popover").snapshot("correct initial position") + createSteps("calcite-popover") + .executeScript( + // we hijack styling to ensure initial positioning is captured in the screenshot + ` + const popover = document.querySelector("calcite-popover"); + popover.style = "border: 1px solid red"; + popover.addAttribute("calcite-hydrated"); + popover.removeAttribute("calcite-hydrated-hidden"); + ` + ) + .snapshot("correct initial position") ); From d7369dfb0b73a126c5b7d75c1bb7741ac1e2e26b Mon Sep 17 00:00:00 2001 From: JC Franco Date: Fri, 14 Oct 2022 17:53:19 -0700 Subject: [PATCH 04/17] fix typo --- src/components/popover/popover.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/popover/popover.stories.ts b/src/components/popover/popover.stories.ts index b3b3c095297..b1dabf0a6a9 100644 --- a/src/components/popover/popover.stories.ts +++ b/src/components/popover/popover.stories.ts @@ -122,7 +122,7 @@ export const correctInitialPosition_TestOnly = stepStory( ` const popover = document.querySelector("calcite-popover"); popover.style = "border: 1px solid red"; - popover.addAttribute("calcite-hydrated"); + popover.setAttribute("calcite-hydrated"); popover.removeAttribute("calcite-hydrated-hidden"); ` ) From b376ae60565bca7e9e87a0da5bf86621a98a7a49 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Sat, 15 Oct 2022 07:58:35 -0700 Subject: [PATCH 05/17] add missing setAttr argument --- src/components/popover/popover.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/popover/popover.stories.ts b/src/components/popover/popover.stories.ts index b1dabf0a6a9..7b658c53aad 100644 --- a/src/components/popover/popover.stories.ts +++ b/src/components/popover/popover.stories.ts @@ -122,7 +122,7 @@ export const correctInitialPosition_TestOnly = stepStory( ` const popover = document.querySelector("calcite-popover"); popover.style = "border: 1px solid red"; - popover.setAttribute("calcite-hydrated"); + popover.setAttribute("calcite-hydrated", ""); popover.removeAttribute("calcite-hydrated-hidden"); ` ) From 8612077ec32cd22d1f2589f93f8ecc885fcf9675 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Sat, 15 Oct 2022 08:43:34 -0700 Subject: [PATCH 06/17] add delay before screenshot test setup --- src/components/popover/popover.stories.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/popover/popover.stories.ts b/src/components/popover/popover.stories.ts index 7b658c53aad..747d5a424e0 100644 --- a/src/components/popover/popover.stories.ts +++ b/src/components/popover/popover.stories.ts @@ -1,7 +1,7 @@ import { select, number, text } from "@storybook/addon-knobs"; import { html } from "../../../support/formatting"; import { boolean, createSteps, stepStory, storyFilters } from "../../../.storybook/helpers"; -import { placements } from "../../utils/floating-ui"; +import { placements, repositionDebounceTimeout } from "../../utils/floating-ui"; import readme from "./readme.md"; import { defaultPopoverPlacement } from "../popover/resources"; import { themesDarkDefault } from "../../../.storybook/utils"; @@ -117,6 +117,7 @@ export const correctInitialPosition_TestOnly = stepStory( `, createSteps("calcite-popover") + .wait(repositionDebounceTimeout * 2) .executeScript( // we hijack styling to ensure initial positioning is captured in the screenshot ` From b11cf609198909030f5c70edbc8c8defc01fac89 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Mon, 17 Oct 2022 11:47:57 -0700 Subject: [PATCH 07/17] revisit approach to preserve debounced internal repositioning calls and correct positioning --- src/components/combobox/combobox.tsx | 47 +- src/components/dropdown/dropdown.tsx | 54 ++- .../input-date-picker/input-date-picker.tsx | 44 +- .../input-time-picker/input-time-picker.tsx | 12 +- src/components/popover/popover.tsx | 62 +-- src/components/tooltip/tooltip.tsx | 53 ++- src/index.html | 438 +----------------- src/utils/floating-ui.spec.ts | 117 ++++- src/utils/floating-ui.ts | 79 +++- stencil.config.ts | 3 +- 10 files changed, 342 insertions(+), 567 deletions(-) diff --git a/src/components/combobox/combobox.tsx b/src/components/combobox/combobox.tsx index 5c0c8412c55..42796c80c0b 100644 --- a/src/components/combobox/combobox.tsx +++ b/src/components/combobox/combobox.tsx @@ -16,7 +16,6 @@ import { filter } from "../../utils/filter"; import { debounce } from "lodash-es"; import { - positionFloatingUI, FloatingCSS, OverlayPositioning, FloatingUIComponent, @@ -26,7 +25,7 @@ import { EffectivePlacement, defaultMenuPlacement, filterComputedPlacements, - repositionDebounceTimeout + reposition } from "../../utils/floating-ui"; import { guid } from "../../utils/guid"; import { DeprecatedEventPayload, Scale } from "../interfaces"; @@ -170,7 +169,7 @@ export class Combobox @Watch("overlayPositioning") overlayPositioningHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -224,7 +223,7 @@ export class Combobox @Watch("flipPlacements") flipPlacementsHandler(): void { this.setFilteredPlacements(); - this.debouncedReposition(); + this.reposition(true); } //-------------------------------------------------------------------------- @@ -256,19 +255,27 @@ export class Combobox // //-------------------------------------------------------------------------- - /** Updates the position of the component. */ + /** + * Updates the position of the component. + * + * @param delayed + */ @Method() - async reposition(): Promise { + async reposition(delayed = false): Promise { const { floatingEl, referenceEl, placement, overlayPositioning, filteredFlipPlacements } = this; - return positionFloatingUI({ - floatingEl, - referenceEl, - overlayPositioning, - placement, - flipPlacements: filteredFlipPlacements, - type: "menu" - }); + return reposition( + this, + { + floatingEl, + referenceEl, + overlayPositioning, + placement, + flipPlacements: filteredFlipPlacements, + type: "menu" + }, + delayed + ); } /** Sets focus on the component. */ @@ -339,7 +346,7 @@ export class Combobox connectForm(this); connectOpenCloseComponent(this); this.setFilteredPlacements(); - this.debouncedReposition(); + this.reposition(true); if (this.active) { this.activeHandler(this.active); } @@ -354,12 +361,12 @@ export class Combobox componentDidLoad(): void { afterConnectDefaultValueSet(this, this.getValue()); - this.debouncedReposition(); + this.reposition(true); } componentDidRender(): void { if (this.el.offsetHeight !== this.inputHeight) { - this.debouncedReposition(); + this.reposition(true); this.inputHeight = this.el.offsetHeight; } @@ -454,8 +461,6 @@ export class Combobox // // -------------------------------------------------------------------------- - private debouncedReposition = debounce(() => this.reposition(), repositionDebounceTimeout); - setFilteredPlacements = (): void => { const { el, flipPlacements } = this; @@ -618,11 +623,11 @@ export class Combobox return; } - await this.debouncedReposition(); + await this.reposition(true); const maxScrollerHeight = this.getMaxScrollerHeight(); listContainerEl.style.maxHeight = maxScrollerHeight > 0 ? `${maxScrollerHeight}px` : ""; listContainerEl.style.minWidth = `${referenceEl.clientWidth}px`; - await this.debouncedReposition(); + await this.reposition(true); }; calciteChipDismissHandler = ( diff --git a/src/components/dropdown/dropdown.tsx b/src/components/dropdown/dropdown.tsx index 13a8872f4f1..0d39faa7272 100644 --- a/src/components/dropdown/dropdown.tsx +++ b/src/components/dropdown/dropdown.tsx @@ -15,7 +15,6 @@ import { ItemKeyboardEvent, Selection } from "./interfaces"; import { focusElement, toAriaBoolean } from "../../utils/dom"; import { - positionFloatingUI, FloatingCSS, OverlayPositioning, FloatingUIComponent, @@ -25,7 +24,7 @@ import { MenuPlacement, defaultMenuPlacement, filterComputedPlacements, - repositionDebounceTimeout + reposition } from "../../utils/floating-ui"; import { Scale } from "../interfaces"; import { SLOTS } from "./resources"; @@ -39,7 +38,6 @@ import { import { guid } from "../../utils/guid"; import { RequestedItem } from "../dropdown-group/interfaces"; import { isActivationKey } from "../../utils/key"; -import { debounce } from "lodash-es"; /** * @slot - A slot for adding `calcite-dropdown-group` components. Every `calcite-dropdown-item` must have a parent `calcite-dropdown-group`, even if the `groupTitle` property is not set. @@ -83,7 +81,7 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float @Watch("open") openHandler(value: boolean): void { if (!this.disabled) { - this.debouncedReposition(); + this.reposition(true); this.active = value; return; } @@ -115,7 +113,7 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float @Watch("flipPlacements") flipPlacementsHandler(): void { this.setFilteredPlacements(); - this.debouncedReposition(); + this.reposition(true); } /** @@ -141,7 +139,7 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float @Watch("overlayPositioning") overlayPositioningHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -153,7 +151,7 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float @Watch("placement") placementHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** specify the scale of dropdown, defaults to m */ @@ -181,7 +179,7 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float connectedCallback(): void { this.mutationObserver?.observe(this.el, { childList: true, subtree: true }); this.setFilteredPlacements(); - this.debouncedReposition(); + this.reposition(true); if (this.open) { this.openHandler(this.open); } @@ -192,7 +190,7 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float } componentDidLoad(): void { - this.debouncedReposition(); + this.reposition(true); } componentDidRender(): void { @@ -254,19 +252,27 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float // //-------------------------------------------------------------------------- - /** Updates the position of the component. */ + /** + * Updates the position of the component. + * + * @param delayed + */ @Method() - async reposition(): Promise { + async reposition(delayed = false): Promise { const { floatingEl, referenceEl, placement, overlayPositioning, filteredFlipPlacements } = this; - return positionFloatingUI({ - floatingEl, - referenceEl, - overlayPositioning, - placement, - flipPlacements: filteredFlipPlacements, - type: "menu" - }); + return reposition( + this, + { + floatingEl, + referenceEl, + overlayPositioning, + placement, + flipPlacements: filteredFlipPlacements, + type: "menu" + }, + delayed + ); } //-------------------------------------------------------------------------- @@ -420,8 +426,6 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float // //-------------------------------------------------------------------------- - private debouncedReposition = debounce(() => this.reposition(), repositionDebounceTimeout); - slotChangeHandler = (event: Event): void => { this.defaultAssignedElements = (event.target as HTMLSlotElement).assignedElements({ flatten: true @@ -443,7 +447,7 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float flatten: true }) as HTMLElement[]; - this.debouncedReposition(); + this.reposition(true); }; updateItems = (): void => { @@ -453,7 +457,7 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float this.updateSelectedItems(); - this.debouncedReposition(); + this.reposition(true); }; updateGroups = (event: Event): void => { @@ -494,10 +498,10 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float return; } - this.debouncedReposition(); + this.reposition(true); const maxScrollerHeight = this.getMaxScrollerHeight(); scrollerEl.style.maxHeight = maxScrollerHeight > 0 ? `${maxScrollerHeight}px` : ""; - this.debouncedReposition(); + this.reposition(true); }; setScrollerAndTransitionEl = (el: HTMLDivElement): void => { diff --git a/src/components/input-date-picker/input-date-picker.tsx b/src/components/input-date-picker/input-date-picker.tsx index 53670c8ca74..2267aded30c 100644 --- a/src/components/input-date-picker/input-date-picker.tsx +++ b/src/components/input-date-picker/input-date-picker.tsx @@ -36,7 +36,6 @@ import { submitForm } from "../../utils/form"; import { - positionFloatingUI, FloatingCSS, OverlayPositioning, FloatingUIComponent, @@ -46,7 +45,7 @@ import { MenuPlacement, defaultMenuPlacement, filterComputedPlacements, - repositionDebounceTimeout + reposition } from "../../utils/floating-ui"; import { DateRangeChange } from "../date-picker/interfaces"; import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive"; @@ -57,7 +56,6 @@ import { disconnectOpenCloseComponent } from "../../utils/openCloseComponent"; import { connectLocalized, disconnectLocalized, LocalizedComponent } from "../../utils/locale"; -import { debounce } from "lodash-es"; @Component({ tag: "calcite-input-date-picker", @@ -133,7 +131,7 @@ export class InputDatePicker @Watch("flipPlacements") flipPlacementsHandler(): void { this.setFilteredPlacements(); - this.debouncedReposition(); + this.reposition(true); } /** @@ -208,7 +206,7 @@ export class InputDatePicker return; } - this.debouncedReposition(); + this.reposition(true); } /** @@ -291,7 +289,7 @@ export class InputDatePicker @Watch("overlayPositioning") overlayPositioningHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -373,19 +371,27 @@ export class InputDatePicker this.startInput?.setFocus(); } - /** Updates the position of the component. */ + /** + * Updates the position of the component. + * + * @param delayed + */ @Method() - async reposition(): Promise { + async reposition(delayed = false): Promise { const { floatingEl, referenceEl, placement, overlayPositioning, filteredFlipPlacements } = this; - return positionFloatingUI({ - floatingEl, - referenceEl, - overlayPositioning, - placement, - flipPlacements: filteredFlipPlacements, - type: "menu" - }); + return reposition( + this, + { + floatingEl, + referenceEl, + overlayPositioning, + placement, + flipPlacements: filteredFlipPlacements, + type: "menu" + }, + delayed + ); } // -------------------------------------------------------------------------- @@ -431,7 +437,7 @@ export class InputDatePicker connectOpenCloseComponent(this); this.setFilteredPlacements(); - this.debouncedReposition(); + this.reposition(true); } async componentWillLoad(): Promise { @@ -441,7 +447,7 @@ export class InputDatePicker } componentDidLoad(): void { - this.debouncedReposition(); + this.reposition(true); } disconnectedCallback(): void { @@ -632,8 +638,6 @@ export class InputDatePicker // //-------------------------------------------------------------------------- - private debouncedReposition = debounce(() => this.reposition(), repositionDebounceTimeout); - setFilteredPlacements = (): void => { const { el, flipPlacements } = this; diff --git a/src/components/input-time-picker/input-time-picker.tsx b/src/components/input-time-picker/input-time-picker.tsx index 4418311537b..6b42de56619 100644 --- a/src/components/input-time-picker/input-time-picker.tsx +++ b/src/components/input-time-picker/input-time-picker.tsx @@ -86,7 +86,7 @@ export class InputTimePicker return; } - this.reposition(); + this.reposition(true); } /** When `true`, interaction is prevented and the component is displayed with lower opacity. */ @@ -370,10 +370,14 @@ export class InputTimePicker this.calciteInputEl?.setFocus(); } - /** Updates the position of the component. */ + /** + * Updates the position of the component. + * + * @param delayed + */ @Method() - async reposition(): Promise { - this.popoverEl?.reposition(); + async reposition(delayed = false): Promise { + this.popoverEl?.reposition(delayed); } // -------------------------------------------------------------------------- diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index 681efb87bef..c3a4a03f522 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -21,7 +21,6 @@ import { defaultPopoverPlacement } from "./resources"; import { - positionFloatingUI, FloatingCSS, OverlayPositioning, FloatingUIComponent, @@ -32,7 +31,7 @@ import { defaultOffsetDistance, filterComputedPlacements, ReferenceElement, - repositionDebounceTimeout + reposition } from "../../utils/floating-ui"; import { guid } from "../../utils/guid"; @@ -45,7 +44,6 @@ import { import { HeadingLevel, Heading } from "../functional/Heading"; import PopoverManager from "./PopoverManager"; -import { debounce } from "lodash-es"; const manager = new PopoverManager(); @@ -114,7 +112,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { @Watch("flipPlacements") flipPlacementsHandler(): void { this.setFilteredPlacements(); - this.debouncedReposition(); + this.reposition(true); } /** @@ -139,7 +137,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { @Watch("offsetDistance") offsetDistanceOffsetHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -149,7 +147,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { @Watch("offsetSkidding") offsetSkiddingHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -159,7 +157,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { @Watch("open") openHandler(): void { - this.debouncedReposition(); + this.reposition(true); this.setExpandedAttr(); } @@ -175,7 +173,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { @Watch("overlayPositioning") overlayPositioningHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -187,7 +185,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { @Watch("placement") placementHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -198,7 +196,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { @Watch("referenceElement") referenceElementHandler(): void { this.setUpReferenceElement(); - this.debouncedReposition(); + this.reposition(true); } /** @@ -262,7 +260,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { if (this.referenceElement && !this.effectiveReferenceElement) { this.setUpReferenceElement(); } - this.debouncedReposition(); + this.reposition(true); this.hasLoaded = true; } @@ -296,9 +294,13 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { // // -------------------------------------------------------------------------- - /** Updates the position of the component. */ + /** + * Updates the position of the component. + * + * @param delayed + */ @Method() - async reposition(): Promise { + async reposition(delayed = false): Promise { const { el, effectiveReferenceElement, @@ -310,19 +312,23 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { offsetSkidding, arrowEl } = this; - - return positionFloatingUI({ - floatingEl: el, - referenceEl: effectiveReferenceElement, - overlayPositioning, - placement, - disableFlip, - flipPlacements: filteredFlipPlacements, - offsetDistance, - offsetSkidding, - arrowEl, - type: "popover" - }); + return reposition( + this, + { + floatingEl: el, + referenceEl: effectiveReferenceElement, + overlayPositioning, + placement, + disableFlip, + flipPlacements: filteredFlipPlacements, + offsetDistance, + offsetSkidding, + includeArrow: !this.disablePointer, + arrowEl, + type: "popover" + }, + delayed + ); } /** @@ -360,8 +366,6 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { // // -------------------------------------------------------------------------- - private debouncedReposition = debounce(() => this.reposition(), repositionDebounceTimeout); - private setTransitionEl = (el): void => { this.transitionEl = el; connectOpenCloseComponent(this); @@ -470,7 +474,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { storeArrowEl = (el: HTMLDivElement): void => { this.arrowEl = el; - this.debouncedReposition(); + this.reposition(true); }; // -------------------------------------------------------------------------- diff --git a/src/components/tooltip/tooltip.tsx b/src/components/tooltip/tooltip.tsx index ed709468b3c..a3f8ebdd049 100644 --- a/src/components/tooltip/tooltip.tsx +++ b/src/components/tooltip/tooltip.tsx @@ -2,8 +2,6 @@ import { Component, Element, Host, Method, Prop, State, Watch, h, VNode } from " import { CSS, ARIA_DESCRIBED_BY } from "./resources"; import { guid } from "../../utils/guid"; import { - positionFloatingUI, - FloatingCSS, OverlayPositioning, FloatingUIComponent, connectFloatingUI, @@ -11,12 +9,12 @@ import { LogicalPlacement, defaultOffsetDistance, ReferenceElement, - repositionDebounceTimeout + reposition, + FloatingCSS } from "../../utils/floating-ui"; import { queryElementRoots, toAriaBoolean } from "../../utils/dom"; import TooltipManager from "./TooltipManager"; -import { debounce } from "lodash-es"; const manager = new TooltipManager(); @@ -50,7 +48,7 @@ export class Tooltip implements FloatingUIComponent { @Watch("offsetDistance") offsetDistanceOffsetHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -60,7 +58,7 @@ export class Tooltip implements FloatingUIComponent { @Watch("offsetSkidding") offsetSkiddingHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -70,7 +68,7 @@ export class Tooltip implements FloatingUIComponent { @Watch("open") openHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -85,7 +83,7 @@ export class Tooltip implements FloatingUIComponent { @Watch("overlayPositioning") overlayPositioningHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -97,7 +95,7 @@ export class Tooltip implements FloatingUIComponent { @Watch("placement") placementHandler(): void { - this.debouncedReposition(); + this.reposition(true); } /** @@ -144,7 +142,7 @@ export class Tooltip implements FloatingUIComponent { if (this.referenceElement && !this.effectiveReferenceElement) { this.setUpReferenceElement(); } - this.debouncedReposition(); + this.reposition(true); this.hasLoaded = true; } @@ -159,9 +157,13 @@ export class Tooltip implements FloatingUIComponent { // // -------------------------------------------------------------------------- - /** Updates the position of the component. */ + /** + * Updates the position of the component. + * + * @param delayed + */ @Method() - async reposition(): Promise { + async reposition(delayed = false): Promise { const { el, effectiveReferenceElement, @@ -172,16 +174,21 @@ export class Tooltip implements FloatingUIComponent { arrowEl } = this; - return positionFloatingUI({ - floatingEl: el, - referenceEl: effectiveReferenceElement, - overlayPositioning, - placement, - offsetDistance, - offsetSkidding, - arrowEl, - type: "tooltip" - }); + return reposition( + this, + { + floatingEl: el, + referenceEl: effectiveReferenceElement, + overlayPositioning, + placement, + offsetDistance, + offsetSkidding, + includeArrow: true, + arrowEl, + type: "tooltip" + }, + delayed + ); } // -------------------------------------------------------------------------- @@ -190,8 +197,6 @@ export class Tooltip implements FloatingUIComponent { // // -------------------------------------------------------------------------- - private debouncedReposition = debounce(() => this.reposition(), repositionDebounceTimeout); - setUpReferenceElement = (warn = true): void => { this.removeReferences(); this.effectiveReferenceElement = this.getReferenceElement(); diff --git a/src/index.html b/src/index.html index 1c0dd38c32e..20adb556906 100644 --- a/src/index.html +++ b/src/index.html @@ -13,431 +13,17 @@ - - - - -
Calcite Components
- - -
-
- - -
- - - -
- -
- - - -
- - - -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- - - -
- - - -
- - - - - - - - - -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
-
- - -
-
- - - -
- - - -
- - - -
- - - - - - - - - -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- - - -
- - - -
- - - - - - - - - - - - -
- - -
-
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- - - -
- - - -
- - - - - - - -
- - - -
- -
- - - -
- -
- - - -
- - - -
- - - -
- - - -
- - - -
- - - - -
-
+ + + +

Floating

+
+
diff --git a/src/utils/floating-ui.spec.ts b/src/utils/floating-ui.spec.ts index 0df17cc8801..9017b6af621 100644 --- a/src/utils/floating-ui.spec.ts +++ b/src/utils/floating-ui.spec.ts @@ -1,10 +1,18 @@ import { - getEffectivePlacement, + cleanupMap, + connectFloatingUI, defaultOffsetDistance, + disconnectFloatingUI, + effectivePlacements, filterComputedPlacements, + FloatingUIComponent, + getEffectivePlacement, placements, - effectivePlacements + positionFloatingUI, + reposition, + repositionDebounceTimeout } from "./floating-ui"; +import { waitForAnimationFrame } from "../tests/utils"; it("should set calcite placement to FloatingUI placement", () => { const el = document.createElement("div"); @@ -26,6 +34,111 @@ it("should set calcite placement to FloatingUI placement", () => { expect(getEffectivePlacement(el, "trailing-end")).toBe("left-end"); }); +describe("repositioning", () => { + let fakeFloatingUiComponent: FloatingUIComponent; + let floatingEl: HTMLDivElement; + let referenceEl: HTMLButtonElement; + let positionOptions: Parameters[0]; + + beforeEach(() => { + fakeFloatingUiComponent = { + open: false, + reposition: async () => { + /* noop */ + }, + overlayPositioning: "absolute", + placement: "auto" + }; + + floatingEl = document.createElement("div"); + referenceEl = document.createElement("button"); + + positionOptions = { + floatingEl, + referenceEl, + overlayPositioning: fakeFloatingUiComponent.overlayPositioning, + placement: fakeFloatingUiComponent.placement, + type: "popover" + }; + }); + + it("repositions only for open components", async () => { + await reposition(fakeFloatingUiComponent, positionOptions); + expect(floatingEl.style.transform).toBe(""); + + fakeFloatingUiComponent.open = true; + + await reposition(fakeFloatingUiComponent, positionOptions); + expect(floatingEl.style.transform).not.toBe(""); + }); + + it("repositions immediately by default", async () => { + fakeFloatingUiComponent.open = true; + + reposition(fakeFloatingUiComponent, positionOptions); + + expect(floatingEl.style.transform).toBe(""); + + await waitForAnimationFrame(); + expect(floatingEl.style.transform).not.toBe(""); + }); + + it("can reposition after a delay", async () => { + fakeFloatingUiComponent.open = true; + + reposition(fakeFloatingUiComponent, positionOptions, true); + + expect(floatingEl.style.transform).toBe(""); + + await new Promise((resolve) => setTimeout(resolve, repositionDebounceTimeout)); + expect(floatingEl.style.transform).not.toBe(""); + }); + + describe("connect/disconnect helpers", () => { + class ResizeObserverStub { + observe(): void { + /* noop */ + } + + unobserve(): void { + /* noop */ + } + + disconnect(): void { + /* noop */ + } + } + + global.ResizeObserver = ResizeObserverStub; + + it("has connectedCallback and disconnectedCallback helpers", () => { + expect(cleanupMap.has(fakeFloatingUiComponent)).toBe(false); + expect(floatingEl.style.position).toBe(""); + + connectFloatingUI(fakeFloatingUiComponent, referenceEl, floatingEl); + + expect(cleanupMap.has(fakeFloatingUiComponent)).toBe(true); + expect(floatingEl.style.position).toBe("absolute"); + + disconnectFloatingUI(fakeFloatingUiComponent, referenceEl, floatingEl); + + expect(cleanupMap.has(fakeFloatingUiComponent)).toBe(false); + expect(floatingEl.style.position).toBe("absolute"); + + fakeFloatingUiComponent.overlayPositioning = "fixed"; + connectFloatingUI(fakeFloatingUiComponent, referenceEl, floatingEl); + + expect(cleanupMap.has(fakeFloatingUiComponent)).toBe(true); + expect(floatingEl.style.position).toBe("fixed"); + + disconnectFloatingUI(fakeFloatingUiComponent, referenceEl, floatingEl); + + expect(cleanupMap.has(fakeFloatingUiComponent)).toBe(false); + expect(floatingEl.style.position).toBe("fixed"); + }); + }); +}); + it("should have correct value for defaultOffsetDistance", () => { expect(defaultOffsetDistance).toBe(6); }); diff --git a/src/utils/floating-ui.ts b/src/utils/floating-ui.ts index c96e630908e..4fb807d290b 100644 --- a/src/utils/floating-ui.ts +++ b/src/utils/floating-ui.ts @@ -1,19 +1,23 @@ import { - computePosition, - Placement, - Strategy, arrow, - flip, - shift, - hide, - offset, autoPlacement, autoUpdate, + computePosition, + flip, + hide, Middleware, + offset, + Placement, + shift, + Strategy, VirtualElement } from "@floating-ui/dom"; import { getElementDir } from "./dom"; +import { debounce } from "lodash-es"; +/** + * Exported for testing purposes only + */ export const repositionDebounceTimeout = 100; export type ReferenceElement = VirtualElement | Element; @@ -162,8 +166,10 @@ export interface FloatingUIComponent { /** * Updates the position of the component. + * + * @param delayed – (internal) when true, it will reposition the component after a delay. the default is false. This is useful for components that have multiple watched properties that schedule repositioning. */ - reposition(): Promise; + reposition(delayed?: boolean): Promise; } export const FloatingCSS = { @@ -268,9 +274,47 @@ export function getEffectivePlacement(floatingEl: HTMLElement, placement: Logica .replace(/trailing/gi, placements[1]) as EffectivePlacement; } +/** + * Convenience function to manage `reposition` calls for FloatingUIComponents that use `positionFloatingUI. + * + * Note: this is not needed for components that use `calcite-popover`. + * + * @param component + * @param options + * @param options.referenceEl + * @param options.floatingEl + * @param options.overlayPositioning + * @param options.placement + * @param options.disableFlip + * @param options.flipPlacements + * @param options.offsetDistance + * @param options.offsetSkidding + * @param options.arrowEl + * @param options.type + * @param delayed + */ +export async function reposition( + component: FloatingUIComponent, + options: Parameters[0], + delayed = false +): Promise { + if (!component.open) { + return; + } + + return delayed ? debouncedReposition(options) : positionFloatingUI(options); +} + +const debouncedReposition = debounce(positionFloatingUI, repositionDebounceTimeout, { + // leading: true, + maxWait: repositionDebounceTimeout +}); + /** * Positions the floating element relative to the reference element. * + * **Note:** exported for testing purposes only + * * @param root0 * @param root0.referenceEl * @param root0.floatingEl @@ -282,6 +326,7 @@ export function getEffectivePlacement(floatingEl: HTMLElement, placement: Logica * @param root0.offsetSkidding * @param root0.arrowEl * @param root0.type + * @param root0.includeArrow */ export async function positionFloatingUI({ referenceEl, @@ -292,6 +337,7 @@ export async function positionFloatingUI({ flipPlacements, offsetDistance, offsetSkidding, + includeArrow = false, arrowEl, type }: { @@ -301,12 +347,14 @@ export async function positionFloatingUI({ placement: LogicalPlacement; disableFlip?: boolean; flipPlacements?: EffectivePlacement[]; + offsetDistance?: number; offsetSkidding?: number; arrowEl?: HTMLElement; + includeArrow?: boolean; type: UIType; }): Promise { - if (!referenceEl || !floatingEl) { + if (!referenceEl || !floatingEl || (includeArrow && !arrowEl)) { return null; } @@ -360,7 +408,12 @@ export async function positionFloatingUI({ }); } -const cleanupMap = new WeakMap void>(); +/** + * Exported for testing purposes only + * + * @internal + */ +export const cleanupMap = new WeakMap void>(); /** * Helper to set up floating element interactions on connectedCallback. @@ -385,11 +438,7 @@ export function connectFloatingUI( cleanupMap.set( component, - autoUpdate(referenceEl, floatingEl, () => { - if (component.open) { - component.reposition(); - } - }) + autoUpdate(referenceEl, floatingEl, () => component.reposition()) ); } diff --git a/stencil.config.ts b/stencil.config.ts index d391598cd30..e46b02a3b25 100644 --- a/stencil.config.ts +++ b/stencil.config.ts @@ -113,7 +113,8 @@ export const create: () => Config = () => ({ ], testing: { moduleNameMapper: { - "^/assets/(.*)$": "/src/tests/iconPathDataStub.ts" + "^/assets/(.*)$": "/src/tests/iconPathDataStub.ts", + "^lodash-es$": "lodash" }, setupFilesAfterEnv: ["/src/tests/setupTests.ts"] }, From 55ccb8870033671bd58a4dbcf10693e656d29e52 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Tue, 18 Oct 2022 22:42:45 -0700 Subject: [PATCH 08/17] drop unnecessary story --- src/components/popover/popover.stories.ts | 28 ----------------------- 1 file changed, 28 deletions(-) diff --git a/src/components/popover/popover.stories.ts b/src/components/popover/popover.stories.ts index 747d5a424e0..0c9db44af82 100644 --- a/src/components/popover/popover.stories.ts +++ b/src/components/popover/popover.stories.ts @@ -101,31 +101,3 @@ export const flipPlacements_TestOnly = (): string => html` document.querySelector(".my-popover").flipPlacements = ["right"]; `; - -export const correctInitialPosition_TestOnly = stepStory( - (): string => html` - - - -

Floating

-
-
- `, - createSteps("calcite-popover") - .wait(repositionDebounceTimeout * 2) - .executeScript( - // we hijack styling to ensure initial positioning is captured in the screenshot - ` - const popover = document.querySelector("calcite-popover"); - popover.style = "border: 1px solid red"; - popover.setAttribute("calcite-hydrated", ""); - popover.removeAttribute("calcite-hydrated-hidden"); - ` - ) - .snapshot("correct initial position") -); From ce4a5cef3de8f7cd95fb9598d769d8b3c1f5a8e7 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Wed, 19 Oct 2022 00:24:20 -0700 Subject: [PATCH 09/17] tidy up --- src/utils/floating-ui.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/floating-ui.ts b/src/utils/floating-ui.ts index 4fb807d290b..6aea61f868a 100644 --- a/src/utils/floating-ui.ts +++ b/src/utils/floating-ui.ts @@ -306,7 +306,6 @@ export async function reposition( } const debouncedReposition = debounce(positionFloatingUI, repositionDebounceTimeout, { - // leading: true, maxWait: repositionDebounceTimeout }); From 92ba61cdd5c4aa9fbcadf064b902c963121c7b3f Mon Sep 17 00:00:00 2001 From: JC Franco Date: Wed, 19 Oct 2022 10:31:18 -0700 Subject: [PATCH 10/17] fix hydrate build --- src/utils/floating-ui.spec.ts | 16 ---------------- src/utils/floating-ui.ts | 12 +++++++++++- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/utils/floating-ui.spec.ts b/src/utils/floating-ui.spec.ts index 9017b6af621..09d2162fd8e 100644 --- a/src/utils/floating-ui.spec.ts +++ b/src/utils/floating-ui.spec.ts @@ -95,22 +95,6 @@ describe("repositioning", () => { }); describe("connect/disconnect helpers", () => { - class ResizeObserverStub { - observe(): void { - /* noop */ - } - - unobserve(): void { - /* noop */ - } - - disconnect(): void { - /* noop */ - } - } - - global.ResizeObserver = ResizeObserverStub; - it("has connectedCallback and disconnectedCallback helpers", () => { expect(cleanupMap.has(fakeFloatingUiComponent)).toBe(false); expect(floatingEl.style.position).toBe(""); diff --git a/src/utils/floating-ui.ts b/src/utils/floating-ui.ts index 6aea61f868a..0d9c05a82a7 100644 --- a/src/utils/floating-ui.ts +++ b/src/utils/floating-ui.ts @@ -14,6 +14,7 @@ import { } from "@floating-ui/dom"; import { getElementDir } from "./dom"; import { debounce } from "lodash-es"; +import { Build } from "@stencil/core"; /** * Exported for testing purposes only @@ -435,9 +436,18 @@ export function connectFloatingUI( // ensure position matches for initial positioning floatingEl.style.position = component.overlayPositioning; + const runAutoUpdate = Build.isBrowser + ? autoUpdate + : (_refEl: HTMLElement, _floatingEl: HTMLElement, updateCallback: Function): (() => void) => { + updateCallback(); + return () => { + /* noop */ + }; + }; + cleanupMap.set( component, - autoUpdate(referenceEl, floatingEl, () => component.reposition()) + runAutoUpdate(referenceEl, floatingEl, () => component.reposition()) ); } From 31ea11a7ecc40b6a706d6a4e9499172685e7874f Mon Sep 17 00:00:00 2001 From: JC Franco Date: Wed, 19 Oct 2022 15:35:29 -0700 Subject: [PATCH 11/17] restore leading option --- src/utils/floating-ui.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/floating-ui.ts b/src/utils/floating-ui.ts index 0d9c05a82a7..7a1e7ae76ba 100644 --- a/src/utils/floating-ui.ts +++ b/src/utils/floating-ui.ts @@ -307,6 +307,7 @@ export async function reposition( } const debouncedReposition = debounce(positionFloatingUI, repositionDebounceTimeout, { + leading: true, maxWait: repositionDebounceTimeout }); From dead5525f611cba0c35f69f5aeef187ed3f0fe37 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Thu, 20 Oct 2022 21:18:02 -0700 Subject: [PATCH 12/17] update test --- src/components/popover/popover.e2e.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/popover/popover.e2e.ts b/src/components/popover/popover.e2e.ts index 93d5c958cfa..f7b8caadc9f 100644 --- a/src/components/popover/popover.e2e.ts +++ b/src/components/popover/popover.e2e.ts @@ -322,13 +322,9 @@ describe("calcite-popover", () => { it("should emit open and beforeOpen events", async () => { const page = await newE2EPage(); - await page.setContent( `content
referenceElement
` ); - - await page.waitForChanges(); - const popover = await page.find("calcite-popover"); const openEvent = await popover.spyOnEvent("calcitePopoverOpen"); @@ -340,10 +336,8 @@ describe("calcite-popover", () => { const popoverOpenEvent = page.waitForEvent("calcitePopoverOpen"); const popoverBeforeOpenEvent = page.waitForEvent("calcitePopoverBeforeOpen"); - await page.evaluate(() => { - const popover = document.querySelector("calcite-popover"); - popover.open = true; - }); + await popover.setProperty("open", true); + await page.waitForChanges(); await popoverOpenEvent; await popoverBeforeOpenEvent; From 1759253014e666579f9799484d6440c57a87d81c Mon Sep 17 00:00:00 2001 From: JC Franco Date: Thu, 20 Oct 2022 21:19:56 -0700 Subject: [PATCH 13/17] tweak positioning * drop scale transformto have correct dimensions initially * reposition immediately on componentDidLoad --- src/assets/styles/_floating-ui.scss | 1 - src/components/popover/popover.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/assets/styles/_floating-ui.scss b/src/assets/styles/_floating-ui.scss index 4cc40d8437d..dec57b574b1 100644 --- a/src/assets/styles/_floating-ui.scss +++ b/src/assets/styles/_floating-ui.scss @@ -80,7 +80,6 @@ $floating-ui-default-z-index: 900; display: block; position: absolute; z-index: $zIndex; - transform: scale(0); } @mixin floatingUIWrapper { diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index c3a4a03f522..eb46145c14a 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -260,7 +260,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { if (this.referenceElement && !this.effectiveReferenceElement) { this.setUpReferenceElement(); } - this.reposition(true); + this.reposition(); this.hasLoaded = true; } From 407636012579ba371ac82dc8af9d2d836f6b0546 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Fri, 21 Oct 2022 10:11:41 -0700 Subject: [PATCH 14/17] update tests --- src/components/popover/popover.e2e.ts | 19 +++++++++---------- src/components/tooltip/tooltip.e2e.ts | 19 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/components/popover/popover.e2e.ts b/src/components/popover/popover.e2e.ts index f7b8caadc9f..95c6aa18ecf 100644 --- a/src/components/popover/popover.e2e.ts +++ b/src/components/popover/popover.e2e.ts @@ -94,26 +94,25 @@ describe("calcite-popover", () => { it("popover positions when referenceElement is set", async () => { const page = await newE2EPage(); - - await page.setContent(`
referenceElement
`); - + await page.setContent( + html` +
referenceElement
` + ); const element = await page.find("calcite-popover"); let computedStyle: CSSStyleDeclaration = await element.getComputedStyle(); - expect(computedStyle.transform).toBe("matrix(0, 0, 0, 0, 0, 0)"); + expect(computedStyle.transform).toBe("none"); - await page.$eval("calcite-popover", (elm: any) => { - const referenceElement = document.createElement("div"); - document.body.appendChild(referenceElement); - elm.referenceElement = referenceElement; + await page.$eval("calcite-popover", (el: HTMLCalcitePopoverElement): void => { + const referenceElement = document.getElementById("ref"); + el.referenceElement = referenceElement; }); - await page.waitForChanges(); computedStyle = await element.getComputedStyle(); - expect(computedStyle.transform).not.toBe("matrix(0, 0, 0, 0, 0, 0)"); + expect(computedStyle.transform).not.toBe("none"); }); it("open popover should be visible", async () => { diff --git a/src/components/tooltip/tooltip.e2e.ts b/src/components/tooltip/tooltip.e2e.ts index 318a7f66307..41d2ef46004 100644 --- a/src/components/tooltip/tooltip.e2e.ts +++ b/src/components/tooltip/tooltip.e2e.ts @@ -67,26 +67,25 @@ describe("calcite-tooltip", () => { it("tooltip positions when referenceElement is set", async () => { const page = await newE2EPage(); - - await page.setContent(`
referenceElement
`); - + await page.setContent( + html` +
referenceElement
` + ); const element = await page.find("calcite-tooltip"); let computedStyle: CSSStyleDeclaration = await element.getComputedStyle(); - expect(computedStyle.transform).toBe("matrix(0, 0, 0, 0, 0, 0)"); + expect(computedStyle.transform).toBe("none"); - await page.$eval("calcite-tooltip", (elm: any) => { - const referenceElement = document.createElement("div"); - document.body.appendChild(referenceElement); - elm.referenceElement = referenceElement; + await page.$eval("calcite-tooltip", (el: HTMLCalciteTooltipElement): void => { + const referenceElement = document.getElementById("ref"); + el.referenceElement = referenceElement; }); - await page.waitForChanges(); computedStyle = await element.getComputedStyle(); - expect(computedStyle.transform).not.toBe("matrix(0, 0, 0, 0, 0, 0)"); + expect(computedStyle.transform).not.toBe("none"); }); it("open tooltip should be visible", async () => { From fb49e3641703898fb18e85ae980372c17c943448 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Fri, 21 Oct 2022 11:34:44 -0700 Subject: [PATCH 15/17] revert index.html updates --- src/index.html | 438 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 426 insertions(+), 12 deletions(-) diff --git a/src/index.html b/src/index.html index 20adb556906..1c0dd38c32e 100644 --- a/src/index.html +++ b/src/index.html @@ -13,17 +13,431 @@ - - - -

Floating

-
-
+ + + + +
Calcite Components
+ + +
+
+ + +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ + + + + + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + +
+
+ + + +
+ + + +
+ + + +
+ + + + + + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ + + + + + + + + + + + +
+ + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ + + + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + + +
+
From 477bd697a070bf861546ad3dc059b8e8da3b3a17 Mon Sep 17 00:00:00 2001 From: JC Franco Date: Fri, 21 Oct 2022 20:31:45 -0700 Subject: [PATCH 16/17] use transitions to reset positioning --- src/components/combobox/combobox.tsx | 8 ++- src/components/dropdown/dropdown.tsx | 13 +++- .../input-date-picker/input-date-picker.tsx | 12 +++- .../input-time-picker/input-time-picker.tsx | 4 +- src/components/popover/popover.tsx | 12 +++- src/components/tooltip/tooltip.tsx | 11 +++- src/utils/floating-ui.ts | 60 ++++++++++++++++++- 7 files changed, 105 insertions(+), 15 deletions(-) diff --git a/src/components/combobox/combobox.tsx b/src/components/combobox/combobox.tsx index 42796c80c0b..2649ca4844f 100644 --- a/src/components/combobox/combobox.tsx +++ b/src/components/combobox/combobox.tsx @@ -25,7 +25,8 @@ import { EffectivePlacement, defaultMenuPlacement, filterComputedPlacements, - reposition + reposition, + updateAfterClose } from "../../utils/floating-ui"; import { guid } from "../../utils/guid"; import { DeprecatedEventPayload, Scale } from "../interfaces"; @@ -114,11 +115,16 @@ export class Combobox @Watch("open") openHandler(value: boolean): void { + if (!value) { + updateAfterClose(this.floatingEl); + } + if (this.disabled) { this.active = false; this.open = false; return; } + this.active = value; this.setMaxScrollerHeight(); } diff --git a/src/components/dropdown/dropdown.tsx b/src/components/dropdown/dropdown.tsx index 0d39faa7272..19728299375 100644 --- a/src/components/dropdown/dropdown.tsx +++ b/src/components/dropdown/dropdown.tsx @@ -24,7 +24,8 @@ import { MenuPlacement, defaultMenuPlacement, filterComputedPlacements, - reposition + reposition, + updateAfterClose } from "../../utils/floating-ui"; import { Scale } from "../interfaces"; import { SLOTS } from "./resources"; @@ -81,11 +82,19 @@ export class Dropdown implements InteractiveComponent, OpenCloseComponent, Float @Watch("open") openHandler(value: boolean): void { if (!this.disabled) { - this.reposition(true); + if (value) { + this.reposition(true); + } else { + updateAfterClose(this.floatingEl); + } this.active = value; return; } + if (!value) { + updateAfterClose(this.floatingEl); + } + this.open = false; } diff --git a/src/components/input-date-picker/input-date-picker.tsx b/src/components/input-date-picker/input-date-picker.tsx index 2267aded30c..567f483fce4 100644 --- a/src/components/input-date-picker/input-date-picker.tsx +++ b/src/components/input-date-picker/input-date-picker.tsx @@ -45,7 +45,8 @@ import { MenuPlacement, defaultMenuPlacement, filterComputedPlacements, - reposition + reposition, + updateAfterClose } from "../../utils/floating-ui"; import { DateRangeChange } from "../date-picker/interfaces"; import { InteractiveComponent, updateHostInteraction } from "../../utils/interactive"; @@ -202,11 +203,18 @@ export class InputDatePicker this.active = value; if (this.disabled || this.readOnly) { + if (!value) { + updateAfterClose(this.floatingEl); + } this.open = false; return; } - this.reposition(true); + if (value) { + this.reposition(true); + } else { + updateAfterClose(this.floatingEl); + } } /** diff --git a/src/components/input-time-picker/input-time-picker.tsx b/src/components/input-time-picker/input-time-picker.tsx index 6b42de56619..cc7de419a32 100644 --- a/src/components/input-time-picker/input-time-picker.tsx +++ b/src/components/input-time-picker/input-time-picker.tsx @@ -86,7 +86,9 @@ export class InputTimePicker return; } - this.reposition(true); + if (value) { + this.reposition(true); + } } /** When `true`, interaction is prevented and the component is displayed with lower opacity. */ diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index eb46145c14a..b632e52f432 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -31,7 +31,8 @@ import { defaultOffsetDistance, filterComputedPlacements, ReferenceElement, - reposition + reposition, + updateAfterClose } from "../../utils/floating-ui"; import { guid } from "../../utils/guid"; @@ -156,8 +157,13 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { @Prop({ reflect: true, mutable: true }) open = false; @Watch("open") - openHandler(): void { - this.reposition(true); + openHandler(value: boolean): void { + if (value) { + this.reposition(true); + } else { + updateAfterClose(this.el); + } + this.setExpandedAttr(); } diff --git a/src/components/tooltip/tooltip.tsx b/src/components/tooltip/tooltip.tsx index a3f8ebdd049..0c3ad53cc1e 100644 --- a/src/components/tooltip/tooltip.tsx +++ b/src/components/tooltip/tooltip.tsx @@ -10,7 +10,8 @@ import { defaultOffsetDistance, ReferenceElement, reposition, - FloatingCSS + FloatingCSS, + updateAfterClose } from "../../utils/floating-ui"; import { queryElementRoots, toAriaBoolean } from "../../utils/dom"; @@ -67,8 +68,12 @@ export class Tooltip implements FloatingUIComponent { @Prop({ reflect: true }) open = false; @Watch("open") - openHandler(): void { - this.reposition(true); + openHandler(value: boolean): void { + if (value) { + this.reposition(true); + } else { + updateAfterClose(this.el); + } } /** diff --git a/src/utils/floating-ui.ts b/src/utils/floating-ui.ts index 7a1e7ae76ba..ee81401081b 100644 --- a/src/utils/floating-ui.ts +++ b/src/utils/floating-ui.ts @@ -12,10 +12,12 @@ import { Strategy, VirtualElement } from "@floating-ui/dom"; -import { getElementDir } from "./dom"; +import { closestElementCrossShadowBoundary, getElementDir } from "./dom"; import { debounce } from "lodash-es"; import { Build } from "@stencil/core"; +const placementDataAttribute = "data-placement"; + /** * Exported for testing purposes only */ @@ -395,7 +397,7 @@ export async function positionFloatingUI({ const visibility = referenceHidden ? "hidden" : null; const pointerEvents = visibility ? "none" : null; - floatingEl.setAttribute("data-placement", effectivePlacement); + floatingEl.setAttribute(placementDataAttribute, effectivePlacement); const transform = `translate(${Math.round(x)}px,${Math.round(y)}px)`; @@ -434,8 +436,14 @@ export function connectFloatingUI( disconnectFloatingUI(component, referenceEl, floatingEl); + const position = component.overlayPositioning; + // ensure position matches for initial positioning - floatingEl.style.position = component.overlayPositioning; + floatingEl.style.position = position; + + if (position === "absolute") { + moveOffScreen(floatingEl); + } const runAutoUpdate = Build.isBrowser ? autoUpdate @@ -468,6 +476,8 @@ export function disconnectFloatingUI( return; } + getTransitionTarget(floatingEl).removeEventListener("transitionend", handleTransitionElTransitionEnd); + const cleanup = cleanupMap.get(component); if (cleanup) { @@ -485,3 +495,47 @@ const visiblePointerSize = 4; * @default 6 */ export const defaultOffsetDistance = Math.ceil(Math.hypot(visiblePointerSize, visiblePointerSize)); + +/** + * This utils applies floating element styles to avoid affecting layout when closed. + * + * This should be called when the closing transition will start. + * + * @param floatingEl + */ +export function updateAfterClose(floatingEl: HTMLElement): void { + if (!floatingEl || floatingEl.style.position !== "absolute") { + return; + } + + getTransitionTarget(floatingEl).addEventListener("transitionend", handleTransitionElTransitionEnd); +} + +function getTransitionTarget(floatingEl: HTMLElement): ShadowRoot | HTMLElement { + // assumes floatingEl w/ shadowRoot is a FloatingUIComponent + return floatingEl.shadowRoot || floatingEl; +} + +function handleTransitionElTransitionEnd(event: TransitionEvent): void { + const floatingTransitionEl = event.target as HTMLElement; + + if ( + // using any prop from floating-ui transition + event.propertyName === "opacity" && + floatingTransitionEl.classList.contains(FloatingCSS.animation) + ) { + const floatingEl = getFloatingElFromTransitionTarget(floatingTransitionEl); + moveOffScreen(floatingEl); + getTransitionTarget(floatingEl).removeEventListener("transitionend", handleTransitionElTransitionEnd); + } +} + +function moveOffScreen(floatingEl: HTMLElement): void { + floatingEl.style.transform = ""; + floatingEl.style.top = "-99999px"; + floatingEl.style.left = "-99999px"; +} + +function getFloatingElFromTransitionTarget(floatingTransitionEl: HTMLElement): HTMLElement { + return closestElementCrossShadowBoundary(floatingTransitionEl, `[${placementDataAttribute}]`); +} From 72876cf747a183c269b7318935b1a2f1fe216257 Mon Sep 17 00:00:00 2001 From: Matt Driscoll Date: Mon, 24 Oct 2022 12:37:10 -0700 Subject: [PATCH 17/17] fix test. --- src/components/popover/popover.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/popover/popover.e2e.ts b/src/components/popover/popover.e2e.ts index 95c6aa18ecf..dbbd12bac19 100644 --- a/src/components/popover/popover.e2e.ts +++ b/src/components/popover/popover.e2e.ts @@ -634,7 +634,7 @@ describe("calcite-popover", () => { it("owns a floating-ui", () => floatingUIOwner( - `content
referenceElement
`, + `content
referenceElement
`, "open" ));