@@ -691,6 +689,52 @@ describe('Tooltip', () => {
});
});
+ describe('Scroll strategy', () => {
+ let container: HTMLElement;
+
+ beforeEach(async () => {
+ container = await fixture(html`
+
+ I have a tooltip
+ It works!
+
+ `);
+ tooltip = container.querySelector(IgcTooltipComponent.tagName)!;
+ });
+
+ it('`scroll` behavior', async () => {
+ tooltip.scrollStrategy = 'scroll';
+ await tooltip.show();
+ await simulateScroll(container, { top: 200 });
+ await hideComplete();
+
+ expect(tooltip.open).to.be.true;
+ });
+
+ it('`close` behavior', async () => {
+ const eventSpy = spy(tooltip, 'emitEvent');
+
+ tooltip.scrollStrategy = 'close';
+ await tooltip.show();
+ await simulateScroll(container, { top: 200 });
+ await hideComplete();
+
+ expect(tooltip.open).to.be.false;
+ expect(eventSpy.firstCall).calledWith('igcClosing');
+ expect(eventSpy.lastCall).calledWith('igcClosed');
+ });
+
+ it('`close` dismisses a sticky tooltip too', async () => {
+ tooltip.sticky = true;
+ tooltip.scrollStrategy = 'close';
+ await tooltip.show();
+ await simulateScroll(container, { top: 200 });
+ await hideComplete();
+
+ expect(tooltip.open).to.be.false;
+ });
+ });
+
describe('Behaviors', () => {
beforeEach(async () => {
clock = useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
diff --git a/src/components/tooltip/tooltip.ts b/src/components/tooltip/tooltip.ts
index ebc2fc708..f98070c6f 100644
--- a/src/components/tooltip/tooltip.ts
+++ b/src/components/tooltip/tooltip.ts
@@ -22,6 +22,7 @@ import IgcIconComponent from '../icon/icon.js';
import IgcPopoverComponent, {
type PopoverPlacement,
} from '../popover/popover.js';
+import type { PopoverScrollStrategy } from '../types.js';
import { addTooltipController } from './controller.js';
import { styles as shared } from './themes/shared/tooltip.common.css.js';
import { all } from './themes/themes.js';
@@ -206,6 +207,22 @@ export default class IgcTooltipComponent extends EventEmitterMixin<
@property()
public placement: PopoverPlacement = 'bottom';
+ /**
+ * Sets the behavior of the tooltip when the parent container scrolls.
+ *
+ * If the value is `hide`, the tooltip hides while the anchor is fully out
+ * of view. `hide` is the default value.
+ *
+ * If the value is `scroll`, the tooltip stays visible and anchored.
+ *
+ * If the value is `close`, the tooltip closes on each scroll. The tooltip
+ * also closes if you set the `sticky` property. The Escape key behaves the
+ * same way.
+ * @attr scroll-strategy
+ */
+ @property({ attribute: 'scroll-strategy' })
+ public scrollStrategy: PopoverScrollStrategy = 'hide';
+
/**
* An element instance or an IDREF to use as the anchor for the tooltip.
*
@@ -511,8 +528,16 @@ export default class IgcTooltipComponent extends EventEmitterMixin<
}
}
- /** Sticky mode close action - closes without waiting out `hideDelay`. */
- private _hideOnCloseClick(): void {
+ /**
+ * Closes the tooltip and emits the events. The method ignores `hideDelay`.
+ * The method also ignores the `sticky` property, unlike
+ * `_hideOnInteraction`.
+ *
+ * The close button of a sticky tooltip calls this method. The `close`
+ * scroll strategy also calls it, because that strategy closes a sticky
+ * tooltip too.
+ */
+ private _hideImmediately(): void {
this._applyTooltipState({ show: false, withEvents: true });
}
@@ -536,17 +561,17 @@ export default class IgcTooltipComponent extends EventEmitterMixin<
.offset=${this.offset}
.anchor=${this._controller.anchor ?? undefined}
.arrowOffset=${this._arrowOffset}
- .shiftPadding=${8}
+ .scrollStrategy=${this.scrollStrategy}
?open=${this.open}
flip
- shift
+ @igcPopoverScrollClose=${this._hideImmediately}
>
${this.message}
${
this.sticky
? html`
-
+
void;
- resetListeners?: boolean;
-};
-
-type RootScrollControllerHost = ReactiveControllerHost & {
- open: boolean;
- hide(): void;
- scrollStrategy?: PopoverScrollStrategy;
-};
-
-type ScrollRecord = { scrollTop: number; scrollLeft: number };
-
-/**
- * `scroll` is not cancelable, so the listener never calls `preventDefault` and
- * is registered as passive to keep it off the scrolling critical path.
- */
-const scrollListenerOptions: AddEventListenerOptions = {
- capture: true,
- passive: true,
-};
-
-function readScroll(element: Element): ScrollRecord {
- return { scrollTop: element.scrollTop, scrollLeft: element.scrollLeft };
-}
-
-function writeScroll(element: Element, record: ScrollRecord): void {
- element.scrollTop = record.scrollTop;
- element.scrollLeft = record.scrollLeft;
-}
-
-class RootScrollController implements ReactiveController {
- private readonly _host: RootScrollControllerHost;
- private _config?: RootScrollControllerConfig;
- private _cache = new WeakMap();
-
- constructor(
- host: RootScrollControllerHost,
- config?: RootScrollControllerConfig
- ) {
- this._host = host;
- this._config = config;
- this._host.addController(this);
- }
-
- private _configureListeners(): void {
- this._host.open ? this._addEventListeners() : this._removeEventListeners();
- }
-
- private _hide(): void {
- this._config?.hideCallback
- ? this._config.hideCallback.call(this._host)
- : this._host.hide();
- }
-
- private _addEventListeners(): void {
- if (this._host.scrollStrategy !== 'scroll') {
- document.addEventListener('scroll', this, scrollListenerOptions);
- }
- }
-
- private _removeEventListeners(): void {
- document.removeEventListener('scroll', this, scrollListenerOptions);
- this._cache = new WeakMap();
- }
-
- /** @internal */
- public handleEvent(event: Event): void {
- this._host.scrollStrategy === 'close' ? this._hide() : this._block(event);
- }
-
- private _block(event: Event): void {
- const element = event.target as Element;
- const child = element.firstElementChild;
-
- let record = this._cache.get(element);
-
- if (!record) {
- record = readScroll(child ?? element);
- this._cache.set(element, record);
- }
-
- writeScroll(element, record);
-
- if (child) {
- writeScroll(child, record);
- }
- }
-
- public update(config?: RootScrollControllerConfig): void {
- if (config) {
- this._config = { ...this._config, ...config };
- }
-
- if (config?.resetListeners) {
- this._removeEventListeners();
- }
-
- this._configureListeners();
- }
-
- /** @internal */
- public hostConnected(): void {
- this._configureListeners();
- }
-
- /** @internal */
- public hostDisconnected(): void {
- this._removeEventListeners();
- }
-}
-
-export function addRootScrollHandler(
- host: RootScrollControllerHost,
- config?: RootScrollControllerConfig
-): RootScrollController {
- return new RootScrollController(host, config);
-}
diff --git a/stories/color-picker.stories.ts b/stories/color-picker.stories.ts
index 473b542aa..4a8004d12 100644
--- a/stories/color-picker.stories.ts
+++ b/stories/color-picker.stories.ts
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import { html } from 'lit';
+import { range } from 'lit/directives/range.js';
import {
IgcColorPickerComponent,
@@ -76,6 +77,14 @@ const metadata: Meta = {
control: { type: 'inline-radio' },
table: { defaultValue: { summary: 'default' } },
},
+ scrollStrategy: {
+ type: { name: 'enum', value: ['scroll', 'hide', 'close'] },
+ description:
+ 'Sets the behavior of the component when the parent container scrolls.\n\nIf the value is `hide`, the component hides while the anchor is fully out\nof view. `hide` is the default value.\n\nIf the value is `scroll`, the component stays visible and anchored.\n\nIf the value is `close`, the component closes on each scroll.',
+ options: ['scroll', 'hide', 'close'],
+ control: { type: 'inline-radio' },
+ table: { defaultValue: { summary: 'hide' } },
+ },
required: {
type: 'boolean',
description:
@@ -112,6 +121,7 @@ const metadata: Meta = {
hideFormats: false,
showAlpha: false,
mode: 'default',
+ scrollStrategy: 'hide',
required: false,
disabled: false,
invalid: false,
@@ -155,6 +165,17 @@ interface IgcColorPickerArgs {
* also opens the picker.
*/
mode: 'default' | 'input';
+ /**
+ * Sets the behavior of the component when the parent container scrolls.
+ *
+ * If the value is `hide`, the component hides while the anchor is fully out
+ * of view. `hide` is the default value.
+ *
+ * If the value is `scroll`, the component stays visible and anchored.
+ *
+ * If the value is `close`, the component closes on each scroll.
+ */
+ scrollStrategy: 'scroll' | 'hide' | 'close';
/** When set, makes the component a required field for validation. */
required: boolean;
/** The name of the control, submitted with the form data. */
@@ -548,3 +569,52 @@ export const Form: Story = {
`,
};
+
+export const InScrollingPanel: Story = {
+ args: {
+ label: 'Accent color',
+ value: '#3f51b5',
+ scrollStrategy: 'close',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'A color picker opens inside a scrolling panel. A panel can be a settings pane, a dialog body or a side drawer. The `scroll-strategy` property sets what happens to the picker when the panel scrolls. If the value is `hide`, the picker hides while the anchor is out of view. `hide` is the default value. If the value is `scroll`, the picker follows the anchor. If the value is `close`, the picker closes.',
+ },
+ },
+ actions: { handles: [] },
+ },
+ render: ({ label, value, mode, scrollStrategy }) => html`
+
+
+
+
Appearance
+
+ Open the picker and scroll this panel to compare the scroll strategies.
+
+
+
+
+
+ ${Array.from(range(1, 24)).map(
+ () => html`The accent color applies to buttons, links and charts. `
+ )}
+
+
+ `,
+};
diff --git a/stories/combo.stories.ts b/stories/combo.stories.ts
index 9d012c3a7..c712edb0a 100644
--- a/stories/combo.stories.ts
+++ b/stories/combo.stories.ts
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import { html } from 'lit';
+import { range } from 'lit/directives/range.js';
import {
type ComboItemTemplate,
@@ -75,6 +76,14 @@ const metadata: Meta = {
"The locale used to resolve the component's resource strings.\nFalls back to the global locale when not set.",
control: 'text',
},
+ scrollStrategy: {
+ type: { name: 'enum', value: ['scroll', 'hide', 'close'] },
+ description:
+ 'Sets the behavior of the component when the parent container scrolls.\n\nIf the value is `hide`, the component hides while the anchor is fully out\nof view. `hide` is the default value.\n\nIf the value is `scroll`, the component stays visible and anchored.\n\nIf the value is `close`, the component closes on each scroll.',
+ options: ['scroll', 'hide', 'close'],
+ control: { type: 'inline-radio' },
+ table: { defaultValue: { summary: 'hide' } },
+ },
label: {
type: 'string',
description: 'The label of the control.',
@@ -170,6 +179,7 @@ const metadata: Meta = {
singleSelect: false,
autofocus: false,
autofocusList: false,
+ scrollStrategy: 'hide',
groupSorting: 'asc',
caseSensitiveIcon: false,
disableFiltering: false,
@@ -197,6 +207,17 @@ interface IgcComboArgs {
* Falls back to the global locale when not set.
*/
locale: string;
+ /**
+ * Sets the behavior of the component when the parent container scrolls.
+ *
+ * If the value is `hide`, the component hides while the anchor is fully out
+ * of view. `hide` is the default value.
+ *
+ * If the value is `scroll`, the component stays visible and anchored.
+ *
+ * If the value is `close`, the component closes on each scroll.
+ */
+ scrollStrategy: 'scroll' | 'hide' | 'close';
/** The label of the control. */
label: string;
/** The placeholder text of the control. */
@@ -590,3 +611,55 @@ export const Form: Story = {
`;
},
};
+
+export const InScrollingPanel: Story = {
+ args: {
+ label: 'Location(s)',
+ placeholder: 'Cities of interest',
+ scrollStrategy: 'close',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'A combo opens its list inside a scrolling panel. A panel can be a settings pane, a dialog body or a side drawer. The `scroll-strategy` property sets what happens to the list when the panel scrolls. If the value is `hide`, the list hides while the input is out of view. `hide` is the default value. If the value is `scroll`, the list follows the input. If the value is `close`, the list closes.',
+ },
+ },
+ },
+ render: ({ label, placeholder, singleSelect, scrollStrategy }) => html`
+
+
+
+
Shipping preferences
+
+ Open the list and scroll this panel to compare the scroll strategies.
+
+
+
+
+
+ ${Array.from(range(1, 24)).map(
+ () => html`Deliveries are grouped by country and dispatched daily. `
+ )}
+
+
+ `,
+};
diff --git a/stories/date-picker.stories.ts b/stories/date-picker.stories.ts
index 6332f9cbb..b30e0322a 100644
--- a/stories/date-picker.stories.ts
+++ b/stories/date-picker.stories.ts
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import { html } from 'lit';
+import { range } from 'lit/directives/range.js';
import {
type DateRangeDescriptor,
@@ -82,6 +83,14 @@ const metadata: Meta = {
control: { type: 'inline-radio' },
table: { defaultValue: { summary: 'dropdown' } },
},
+ scrollStrategy: {
+ type: { name: 'enum', value: ['scroll', 'hide', 'close'] },
+ description:
+ 'Sets the behavior of the component when the parent container scrolls.\n\nIf the value is `hide`, the component hides while the anchor is fully out\nof view. `hide` is the default value.\n\nIf the value is `scroll`, the component stays visible and anchored.\n\nIf the value is `close`, the component closes on each scroll.\n\nIn the `dialog` mode the picker ignores this property, because a scroll\ndoes not move a modal dialog.',
+ options: ['scroll', 'hide', 'close'],
+ control: { type: 'inline-radio' },
+ table: { defaultValue: { summary: 'hide' } },
+ },
readOnly: {
type: 'boolean',
description: 'Makes the control a readonly field.',
@@ -237,6 +246,7 @@ const metadata: Meta = {
disabled: false,
invalid: false,
mode: 'dropdown',
+ scrollStrategy: 'hide',
readOnly: false,
nonEditable: false,
outlined: false,
@@ -278,6 +288,20 @@ interface IgcDatePickerArgs {
invalid: boolean;
/** Determines whether the calendar is opened in a dropdown or a modal dialog. */
mode: 'dropdown' | 'dialog';
+ /**
+ * Sets the behavior of the component when the parent container scrolls.
+ *
+ * If the value is `hide`, the component hides while the anchor is fully out
+ * of view. `hide` is the default value.
+ *
+ * If the value is `scroll`, the component stays visible and anchored.
+ *
+ * If the value is `close`, the component closes on each scroll.
+ *
+ * In the `dialog` mode the picker ignores this property, because a scroll
+ * does not move a modal dialog.
+ */
+ scrollStrategy: 'scroll' | 'hide' | 'close';
/** Makes the control a readonly field. */
readOnly: boolean;
/** Whether to allow typing in the input. */
@@ -634,3 +658,50 @@ export const Form: Story = {
`,
};
+
+export const InScrollingPanel: Story = {
+ args: {
+ label: 'Delivery date',
+ scrollStrategy: 'close',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'A date picker opens its calendar inside a scrolling panel. A panel can be a settings pane, a dialog body or a side drawer. The `scroll-strategy` property sets what happens to the calendar when the panel scrolls. If the value is `hide`, the calendar hides while the input is out of view. `hide` is the default value. If the value is `scroll`, the calendar follows the input. If the value is `close`, the calendar closes. In the `dialog` mode the calendar opens in a modal dialog and not in a popover. The picker then ignores this property. Change the `mode` control to see this behavior.',
+ },
+ },
+ },
+ render: ({ label, mode, scrollStrategy }) => html`
+
+
+
+
Order details
+
+ Open the calendar and scroll this panel to compare the scroll
+ strategies.
+
+
+
+
+
+ ${Array.from(range(1, 24)).map(
+ () => html`Orders placed before noon ship on the selected date. `
+ )}
+
+
+ `,
+};
diff --git a/stories/date-range-picker.stories.ts b/stories/date-range-picker.stories.ts
index 8da603d8d..22ad0ace1 100644
--- a/stories/date-range-picker.stories.ts
+++ b/stories/date-range-picker.stories.ts
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit';
+import { range } from 'lit/directives/range.js';
import {
type DateRangeDescriptor,
@@ -129,6 +130,14 @@ const metadata: Meta = {
control: { type: 'inline-radio' },
table: { defaultValue: { summary: 'dropdown' } },
},
+ scrollStrategy: {
+ type: { name: 'enum', value: ['scroll', 'hide', 'close'] },
+ description:
+ 'Sets the behavior of the component when the parent container scrolls.\n\nIf the value is `hide`, the component hides while the anchor is fully out\nof view. `hide` is the default value.\n\nIf the value is `scroll`, the component stays visible and anchored.\n\nIf the value is `close`, the component closes on each scroll.\n\nIn the `dialog` mode the picker ignores this property, because a scroll\ndoes not move a modal dialog.',
+ options: ['scroll', 'hide', 'close'],
+ control: { type: 'inline-radio' },
+ table: { defaultValue: { summary: 'hide' } },
+ },
readOnly: {
type: 'boolean',
description: 'Makes the control a readonly field.',
@@ -285,6 +294,7 @@ const metadata: Meta = {
disabled: false,
invalid: false,
mode: 'dropdown',
+ scrollStrategy: 'hide',
readOnly: false,
nonEditable: false,
outlined: false,
@@ -332,6 +342,20 @@ interface IgcDateRangePickerArgs {
invalid: boolean;
/** Determines whether the calendar is opened in a dropdown or a modal dialog. */
mode: 'dropdown' | 'dialog';
+ /**
+ * Sets the behavior of the component when the parent container scrolls.
+ *
+ * If the value is `hide`, the component hides while the anchor is fully out
+ * of view. `hide` is the default value.
+ *
+ * If the value is `scroll`, the component stays visible and anchored.
+ *
+ * If the value is `close`, the component closes on each scroll.
+ *
+ * In the `dialog` mode the picker ignores this property, because a scroll
+ * does not move a modal dialog.
+ */
+ scrollStrategy: 'scroll' | 'hide' | 'close';
/** Makes the control a readonly field. */
readOnly: boolean;
/** Whether to allow typing in the input. */
@@ -903,3 +927,51 @@ export const FormSingleInput: Story = {
`,
};
+
+export const InScrollingPanel: Story = {
+ args: {
+ label: 'Stay period',
+ scrollStrategy: 'close',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'A date range picker opens its calendar inside a scrolling panel. A panel can be a settings pane, a dialog body or a side drawer. The `scroll-strategy` property sets what happens to the calendar when the panel scrolls. If the value is `hide`, the calendar hides while the input is out of view. `hide` is the default value. If the value is `scroll`, the calendar follows the input. If the value is `close`, the calendar closes. In the `dialog` mode the calendar opens in a modal dialog and not in a popover. The picker then ignores this property. Change the `mode` control to see this behavior.',
+ },
+ },
+ },
+ render: ({ label, mode, useTwoInputs, scrollStrategy }) => html`
+
+
+
+
Booking details
+
+ Open the calendar and scroll this panel to compare the scroll
+ strategies.
+
+
+
+
+
+ ${Array.from(range(1, 24)).map(
+ () => html`Rates are calculated per night for the selected period. `
+ )}
+
+
+ `,
+};
diff --git a/stories/dropdown.stories.ts b/stories/dropdown.stories.ts
index f0567cd85..7b881afc1 100644
--- a/stories/dropdown.stories.ts
+++ b/stories/dropdown.stories.ts
@@ -99,12 +99,12 @@ const metadata: Meta = {
table: { defaultValue: { summary: 'bottom-start' } },
},
scrollStrategy: {
- type: { name: 'enum', value: ['scroll', 'block', 'close'] },
+ type: { name: 'enum', value: ['scroll', 'hide', 'close'] },
description:
- 'Determines the behavior of the component during scrolling of the parent container.',
- options: ['scroll', 'block', 'close'],
+ 'Sets the behavior of the component when the parent container scrolls.\n\nIf the value is `hide`, the component hides while the anchor is fully out\nof view. `hide` is the default value.\n\nIf the value is `scroll`, the component stays visible and anchored.\n\nIf the value is `close`, the component closes on each scroll.',
+ options: ['scroll', 'hide', 'close'],
control: { type: 'inline-radio' },
- table: { defaultValue: { summary: 'scroll' } },
+ table: { defaultValue: { summary: 'hide' } },
},
flip: {
type: 'boolean',
@@ -149,7 +149,7 @@ const metadata: Meta = {
},
args: {
placement: 'bottom-start',
- scrollStrategy: 'scroll',
+ scrollStrategy: 'hide',
flip: false,
distance: 0,
sameWidth: false,
@@ -176,8 +176,17 @@ interface IgcDropdownArgs {
| 'left'
| 'left-start'
| 'left-end';
- /** Determines the behavior of the component during scrolling of the parent container. */
- scrollStrategy: 'scroll' | 'block' | 'close';
+ /**
+ * Sets the behavior of the component when the parent container scrolls.
+ *
+ * If the value is `hide`, the component hides while the anchor is fully out
+ * of view. `hide` is the default value.
+ *
+ * If the value is `scroll`, the component stays visible and anchored.
+ *
+ * If the value is `close`, the component closes on each scroll.
+ */
+ scrollStrategy: 'scroll' | 'hide' | 'close';
/**
* Whether the component should be flipped to the opposite side of the target once it's about to overflow the visible area.
* When true, once enough space is detected on its preferred side, it will flip back.
@@ -805,13 +814,13 @@ const timeZones = Array.from(range(-11, 13)).flatMap((offset) =>
export const InScrollingPanel: Story = {
args: {
sameWidth: false,
- scrollStrategy: 'block',
+ scrollStrategy: 'close',
},
parameters: {
docs: {
description: {
story:
- 'A long list opened from inside a scrolling panel - a settings pane, a dialog body, a side drawer. The list height is capped with `::part(list)` so it scrolls on its own, and `scroll-strategy` decides what scrolling the panel underneath does to it: `scroll` lets it follow, `block` freezes the panel, `close` dismisses the list.',
+ 'A long list opens inside a scrolling panel. A panel can be a settings pane, a dialog body or a side drawer. The `::part(list)` rule limits the height of the list, so the list scrolls on its own. The `scroll-strategy` property sets what happens to the list when the panel scrolls. If the value is `hide`, the list hides while the target is out of view. `hide` is the default value. If the value is `scroll`, the list follows the target. If the value is `close`, the list closes.',
},
},
},
diff --git a/stories/select.stories.ts b/stories/select.stories.ts
index a921544c3..4b01464f1 100644
--- a/stories/select.stories.ts
+++ b/stories/select.stories.ts
@@ -11,6 +11,7 @@ import {
registerIconFromText,
} from 'igniteui-webcomponents';
import { html } from 'lit';
+import { range } from 'lit/directives/range.js';
import {
disableStoryControls,
formControls,
@@ -115,12 +116,12 @@ const metadata: Meta = {
table: { defaultValue: { summary: 'bottom-start' } },
},
scrollStrategy: {
- type: { name: 'enum', value: ['scroll', 'block', 'close'] },
+ type: { name: 'enum', value: ['scroll', 'hide', 'close'] },
description:
- 'Determines the behavior of the component during scrolling of the parent container.',
- options: ['scroll', 'block', 'close'],
+ 'Sets the behavior of the component when the parent container scrolls.\n\nIf the value is `hide`, the component hides while the anchor is fully out\nof view. `hide` is the default value.\n\nIf the value is `scroll`, the component stays visible and anchored.\n\nIf the value is `close`, the component closes on each scroll.',
+ options: ['scroll', 'hide', 'close'],
control: { type: 'inline-radio' },
- table: { defaultValue: { summary: 'scroll' } },
+ table: { defaultValue: { summary: 'hide' } },
},
required: {
type: 'boolean',
@@ -172,7 +173,7 @@ const metadata: Meta = {
autofocus: false,
distance: 0,
placement: 'bottom-start',
- scrollStrategy: 'scroll',
+ scrollStrategy: 'hide',
required: false,
disabled: false,
invalid: false,
@@ -211,8 +212,17 @@ interface IgcSelectArgs {
| 'left'
| 'left-start'
| 'left-end';
- /** Determines the behavior of the component during scrolling of the parent container. */
- scrollStrategy: 'scroll' | 'block' | 'close';
+ /**
+ * Sets the behavior of the component when the parent container scrolls.
+ *
+ * If the value is `hide`, the component hides while the anchor is fully out
+ * of view. `hide` is the default value.
+ *
+ * If the value is `scroll`, the component stays visible and anchored.
+ *
+ * If the value is `close`, the component closes on each scroll.
+ */
+ scrollStrategy: 'scroll' | 'hide' | 'close';
/** When set, makes the component a required field for validation. */
required: boolean;
/** The name of the control, submitted with the form data. */
@@ -653,3 +663,54 @@ export const Form: Story = {
`;
},
};
+
+export const InScrollingPanel: Story = {
+ args: {
+ label: 'Assign task',
+ scrollStrategy: 'close',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'A select opens its dropdown inside a scrolling panel. A panel can be a settings pane, a dialog body or a side drawer. The `scroll-strategy` property sets what happens to the dropdown when the panel scrolls. If the value is `hide`, the dropdown hides while the input is out of view. `hide` is the default value. If the value is `scroll`, the dropdown follows the input. If the value is `close`, the dropdown closes.',
+ },
+ },
+ },
+ render: ({ label, placement, distance, scrollStrategy }) => html`
+
+
+
+
Sprint planning
+
+ Open the dropdown and scroll this panel to compare the scroll
+ strategies.
+
+
+
+ Available tasks:
+ ${items}
+
+
+
+ ${Array.from(range(1, 24)).map(
+ () => html`Unassigned tasks stay in the backlog until triage. `
+ )}
+
+
+ `,
+};
diff --git a/stories/tooltip.stories.ts b/stories/tooltip.stories.ts
index d77a56a76..e9febe6c7 100644
--- a/stories/tooltip.stories.ts
+++ b/stories/tooltip.stories.ts
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import { html, nothing } from 'lit';
+import { range } from 'lit/directives/range.js';
import { createRef, ref } from 'lit/directives/ref.js';
import {
@@ -98,6 +99,14 @@ const metadata: Meta = {
control: { type: 'select' },
table: { defaultValue: { summary: 'bottom' } },
},
+ scrollStrategy: {
+ type: { name: 'enum', value: ['scroll', 'hide', 'close'] },
+ description:
+ 'Sets the behavior of the tooltip when the parent container scrolls.\n\nIf the value is `hide`, the tooltip hides while the anchor is fully out\nof view. `hide` is the default value.\n\nIf the value is `scroll`, the tooltip stays visible and anchored.\n\nIf the value is `close`, the tooltip closes on each scroll. The tooltip\nalso closes if you set the `sticky` property. The Escape key behaves the\nsame way.',
+ options: ['scroll', 'hide', 'close'],
+ control: { type: 'inline-radio' },
+ table: { defaultValue: { summary: 'hide' } },
+ },
anchor: {
type: { name: 'other', value: 'Element | string' },
description:
@@ -150,6 +159,7 @@ const metadata: Meta = {
withArrow: false,
offset: 6,
placement: 'bottom',
+ scrollStrategy: 'hide',
showTriggers: 'pointerenter,focusin',
hideTriggers: 'pointerleave,click,focusout',
showDelay: 200,
@@ -182,6 +192,19 @@ interface IgcTooltipArgs {
| 'left'
| 'left-start'
| 'left-end';
+ /**
+ * Sets the behavior of the tooltip when the parent container scrolls.
+ *
+ * If the value is `hide`, the tooltip hides while the anchor is fully out
+ * of view. `hide` is the default value.
+ *
+ * If the value is `scroll`, the tooltip stays visible and anchored.
+ *
+ * If the value is `close`, the tooltip closes on each scroll. The tooltip
+ * also closes if you set the `sticky` property. The Escape key behaves the
+ * same way.
+ */
+ scrollStrategy: 'scroll' | 'hide' | 'close';
/** An element instance or an IDREF to use as the anchor for the tooltip. */
anchor: Element | string;
/**
@@ -489,7 +512,7 @@ export const Placements: Story = {
docs: {
description: {
story:
- 'All twelve placements at once - every tooltip below is bound to the same anchor. `placement` picks the side and the alignment along it, `offset` sets the gap to the anchor, and `with-arrow` renders the arrow, which is nudged towards the aligned edge on the `-start` and `-end` variants. The placement is a preference, not a guarantee: the tooltip flips and shifts to stay in the viewport, so scroll the card to an edge to see it move.',
+ 'All twelve placements at once - every tooltip below is bound to the same anchor. `placement` picks the side and the alignment along it, `offset` sets the gap to the anchor, and `with-arrow` renders the arrow, which is nudged towards the aligned edge on the `-start` and `-end` variants. The placement is a preference, not a guarantee: the tooltip flips to the opposite side to stay in the viewport, so scroll the card to an edge to see it move.',
},
},
},
@@ -1183,3 +1206,56 @@ export const CancelingEvents: Story = {
`;
},
};
+
+export const InScrollingPanel: Story = {
+ args: {
+ message: 'Publishes the draft and notifies the reviewers.',
+ sticky: true,
+ withArrow: true,
+ scrollStrategy: 'close',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'A tooltip has its anchor inside a scrolling panel. With the default triggers the tooltip hides when the pointer leaves the anchor. This story sets `sticky`, so the tooltip stays open and you can compare the strategies. The `scroll-strategy` property sets what happens to the tooltip when the panel scrolls. If the value is `hide`, the tooltip hides while the anchor is out of view. `hide` is the default value. If the value is `scroll`, the tooltip follows the anchor. If the value is `close`, the tooltip closes on each scroll. It also closes a sticky tooltip. The Escape key behaves the same way.',
+ },
+ },
+ },
+ render: ({ message, sticky, withArrow, placement, scrollStrategy }) => html`
+
+
+
+
Review queue
+
+ Show the tooltip, then scroll this panel to compare the scroll
+ strategies.
+
+
+
Publish
+
+
+
+ ${Array.from(range(1, 24)).map(
+ () => html`Drafts wait for two approvals before publishing. `
+ )}
+
+
+ `,
+};