Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/react/src/components/OverflowMenu/OverflowMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ export const getMenuOffset: MenuOffset = (
direction
);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- https://github.com/carbon-design-system/carbon/issues/20452
const { offsetWidth: menuWidth, offsetHeight: menuHeight } = menuBody;

const { offsetWidth: menuWidth } = menuBody;

switch (triggerButtonPositionProp) {
case 'top':
Expand Down Expand Up @@ -202,7 +202,7 @@ export interface OverflowMenuProps
menuOffset?: MenuOffset;

/**
* The adjustment in position applied to the floating menu.
* The adjustment in position applied to the floating menu when flipped.
*/
menuOffsetFlip?: MenuOffset;

Expand Down Expand Up @@ -734,7 +734,7 @@ OverflowMenu.propTypes = {
]),

/**
* The adjustment in position applied to the floating menu.
* The adjustment in position applied to the floating menu when flipped.
*/
menuOffsetFlip: PropTypes.oneOfType([
PropTypes.shape({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright IBM Corp. 2019, 2023
* Copyright IBM Corp. 2019, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
Expand All @@ -9,7 +9,7 @@ import { LitElement, html } from 'lit';
import { prefix } from '../../globals/settings';
import styles from './breadcrumb.scss?lit';
import { carbonElement as customElement } from '../../globals/decorators/carbon-element';

import type CDSOverflowMenuBody from '../overflow-menu/overflow-menu-body';
/**
* Breadcrumb item.
*
Expand All @@ -21,24 +21,31 @@ class CDSBreadcrumbItem extends LitElement {
* Handles `slotchange` event.
*/
private _handleSlotChange({ target }: Event) {
if (this.getAttribute('size')) {
const items = (target as HTMLSlotElement)
.assignedNodes()
.filter(
(node) =>
node.nodeType === Node.ELEMENT_NODE &&
(node as Element).tagName.toLowerCase() ===
`${prefix}-overflow-menu`
);
const items = (target as HTMLSlotElement)
.assignedNodes()
.filter(
(node) =>
node.nodeType === Node.ELEMENT_NODE &&
(node as Element).tagName.toLowerCase() === `${prefix}-overflow-menu`
);

items.forEach((item) => {
items.forEach((item) => {
if (this.getAttribute('size')) {
(item as HTMLElement).setAttribute(
'breadcrumb-size',
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- https://github.com/carbon-design-system/carbon/issues/20452
this.getAttribute('size')!
);
});
}
}

const overflowMenuBody = (item as HTMLElement).querySelector(
`${prefix}-overflow-menu-body`
) as CDSOverflowMenuBody;

if (overflowMenuBody) {
overflowMenuBody.menuOffset = { top: 10, left: 59 };
}
});
}

connectedCallback() {
Expand Down
12 changes: 11 additions & 1 deletion packages/web-components/src/components/floating-menu/defs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright IBM Corp. 2020, 2022, 2023
* Copyright IBM Corp. 2020, 2022, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
Expand Down Expand Up @@ -33,4 +33,14 @@ export enum FLOATING_MENU_DIRECTION {
* Put menu body at the bottom of its trigger button.
*/
BOTTOM = 'bottom',

/**
* Put menu body to the left of its trigger button.
*/
LEFT = 'left',

/**
* Put menu body to the right of its trigger button.
*/
RIGHT = 'right',
}
195 changes: 143 additions & 52 deletions packages/web-components/src/components/floating-menu/floating-menu.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright IBM Corp. 2019, 2024
* Copyright IBM Corp. 2019, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
Expand All @@ -19,6 +19,53 @@ import { prefix } from '../../globals/settings';

export { FLOATING_MENU_DIRECTION, FLOATING_MENU_POSITION_DIRECTION };

export interface Offset {
top: number;
left: number;
}

export type MenuDirection = FLOATING_MENU_DIRECTION;

export type MenuOffset =
| Offset
| ((
menuBody: HTMLElement,
menuDirection: MenuDirection,
trigger?: HTMLElement | null,
flipped?: boolean
) => Offset);

/**
* Calculates the offset for the floating menu.
*
* @param menuBody - The menu body element.
* @param menuDirection - The floating menu direction.
* @param trigger - The trigger element.
* @param flipped - Whether the menu is flipped.
* @returns The adjustment of the floating menu position.
*/
export const getMenuOffset = (
menuBody: HTMLElement,
menuDirection: MenuDirection,
trigger?: HTMLElement,
flipped?: boolean
): Offset => {
const { offsetWidth: menuWidth } = menuBody;

switch (menuDirection) {
case FLOATING_MENU_DIRECTION.TOP:
case FLOATING_MENU_DIRECTION.BOTTOM: {
const triggerWidth = !trigger ? 0 : trigger.offsetWidth;
return {
left: (!flipped ? 1 : -1) * (menuWidth / 2 - triggerWidth / 2),
top: 0,
};
}
default:
return { left: 0, top: 0 };
}
};

/**
* Position of floating menu, or trigger button of floating menu.
*/
Expand Down Expand Up @@ -98,10 +145,7 @@ abstract class CDSFloatingMenu extends HostListenerMixin(
private _resizeObserver = new ResizeObserver(() => {
const { container, open, parent, position } = this;
if (container && open && parent) {
const { direction, start, top } = position;
this.style[
direction !== FLOATING_MENU_POSITION_DIRECTION.RTL ? 'left' : 'right'
] = `${start}px`;
const { top } = position;
this.style.top = `${top}px`;
}
});
Expand Down Expand Up @@ -163,6 +207,37 @@ abstract class CDSFloatingMenu extends HostListenerMixin(
*/
abstract flipped: boolean;

/**
* Gets the menu offset configuration (object or function).
* Subclasses override this to specify custom offset configs.
*
* @returns The menu offset configuration, or undefined for no offset.
*/
protected getOffsetConfig(): MenuOffset | undefined {
return undefined;
}

/**
* Resolves the final menu offset by evaluating the offset configuration.
* Handles both static offset objects and dynamic offset functions.
*
* @returns The resolved offset with left and top values.
*/
protected resolveOffset(): Offset {
const config = this.getOffsetConfig();

if (!config) {
return { left: 0, top: 0 };
}

if (typeof config === 'function') {
const trigger = this.parent as HTMLElement;
return config(this, this.direction, trigger, this.flipped);
}

return config;
}

/**
* The DOM element to put this menu into.
*/
Expand Down Expand Up @@ -191,71 +266,88 @@ abstract class CDSFloatingMenu extends HostListenerMixin(
left: refLeft = 0,
top: refTop = 0,
right: refRight = 0,
bottom: refBottom = 0,
} = triggerPosition;
let { bottom: refBottom = 0 } = triggerPosition;
const { width, height } = this.getBoundingClientRect();
const {
left: containerLeft = 0,
right: containerRight = 0,
top: containerTop = 0,
} = container.getBoundingClientRect();
refBottom = refBottom - containerTop;
const containerRect = container.getBoundingClientRect();

const containerComputedStyle =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- https://github.com/carbon-design-system/carbon/issues/20452
container.ownerDocument!.defaultView!.getComputedStyle(container);
const containerPosition =
containerComputedStyle.getPropertyValue('position');
const positionDirection = containerComputedStyle.getPropertyValue(
'direction'
) as FLOATING_MENU_POSITION_DIRECTION;
const isRtl = positionDirection === FLOATING_MENU_POSITION_DIRECTION.RTL;
const containerStartFromViewport = !isRtl
? containerLeft
: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- https://github.com/carbon-design-system/carbon/issues/20452
container.ownerDocument!.defaultView!.innerWidth - containerRight;
const refStartFromContainer = !isRtl
? refLeft - containerLeft
: containerRight - refRight;
const refEndFromContainer = !isRtl
? refRight - containerLeft
: containerRight - refLeft;
const refTopFromContainer = refTop - containerTop;

if (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- https://github.com/carbon-design-system/carbon/issues/20452
(container !== this.ownerDocument!.body ||
containerStartFromViewport !== 0 ||
containerTop !== 0) &&
containerComputedStyle.getPropertyValue('position') === 'static'
) {
throw new Error(
'Floating menu container must not have `position:static`.'
);
}
const scrollX = globalThis.scrollX ?? 0;
const scrollY = globalThis.scrollY ?? 0;
const effectiveScrollX = containerPosition !== 'static' ? 0 : scrollX;
const effectiveScrollY = containerPosition !== 'static' ? 0 : scrollY;

const relativeDiff = {
top: containerPosition !== 'static' ? containerRect.top : 0,
left: containerPosition !== 'static' ? containerRect.left : 0,
};

const refCenterHorizontal = (refLeft + refRight) / 2;
const refCenterVertical = (refTop + refBottom) / 2;

const offset = this.resolveOffset();
const { top = 0, left = 0 } = offset;

const { flipped, direction } = this;
const { direction } = this;
if (Object.values(FLOATING_MENU_DIRECTION).indexOf(direction) < 0) {
throw new Error(`Wrong menu position direction: ${direction}`);
}

const alignmentStart = flipped
? refEndFromContainer - width
: refStartFromContainer;

const { start, top } = {
const positions: Record<MenuDirection, () => Offset> = {
[FLOATING_MENU_DIRECTION.LEFT]: () => ({
left: refLeft - width + effectiveScrollX - left - relativeDiff.left,
top:
refCenterVertical -
height / 2 +
effectiveScrollY +
top -
9 -
relativeDiff.top,
}),
[FLOATING_MENU_DIRECTION.TOP]: () => ({
start: alignmentStart,
top: refTopFromContainer - height,
left:
refCenterHorizontal -
width / 2 +
effectiveScrollX +
left -
relativeDiff.left,
top: refTop - height + effectiveScrollY - top - relativeDiff.top,
}),
[FLOATING_MENU_DIRECTION.RIGHT]: () => ({
left: refRight + effectiveScrollX + left - relativeDiff.left,
top:
refCenterVertical -
height / 2 +
effectiveScrollY +
top +
3 -
relativeDiff.top,
}),
[FLOATING_MENU_DIRECTION.BOTTOM]: () => ({
start: alignmentStart,
top: refBottom,
left:
refCenterHorizontal -
width / 2 +
effectiveScrollX +
left -
relativeDiff.left,
top: refBottom + effectiveScrollY + top - relativeDiff.top,
}),
}[direction]();
};

const { left: calculatedLeft, top: calculatedTop } = positions[direction]();

return {
direction: positionDirection,
start,
top,
start: calculatedLeft,
top: calculatedTop,
};
}

Expand All @@ -281,10 +373,9 @@ abstract class CDSFloatingMenu extends HostListenerMixin(
container.appendChild(this);
}
// Note: `this.position` cannot be referenced until `this.parent` is set
const { direction, start, top } = this.position;
this.style[
direction !== FLOATING_MENU_POSITION_DIRECTION.RTL ? 'left' : 'right'
] = `${start}px`;
const { start, top } = this.position;
this.style.left = `${start}px`;
this.style.right = 'auto';
this.style.top = `${top}px`;
}
if (changedProperties.has('open')) {
Expand Down
Loading
Loading