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
2 changes: 2 additions & 0 deletions src/components/common/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export type TileManagerContext = {
instance: IgcTileManagerComponent;
/** The internal CSS grid container of the igc-tile-manager. */
grid: Ref<HTMLElement>;
/** Synchronizes the tile manager with the maximized state of its tiles. */
setMaximizedState: () => void;
};

const carouselContext = createContext<IgcCarouselComponent>(
Expand Down
24 changes: 24 additions & 0 deletions src/components/common/controllers/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,11 @@ class DragController implements ReactiveController {
Object.assign(this._element.style, {
touchAction: cssValue,
userSelect: cssValue,
webkitUserSelect: cssValue,
});

this._toggleTextSelection(enabled);

enabled
? this._element.setPointerCapture(this._id)
: this._element.releasePointerCapture(this._id);
Expand All @@ -363,6 +366,27 @@ class DragController implements ReactiveController {
}
}

/**
* Toggles text selection for the duration of a drag operation.
*
* @remarks
* Disabling `user-select` only on the dragged element is not enough, since browsers
* (notably Safari) will still create a text selection in whatever elements sit under
* the pointer while dragging. Applying it to the owner document's body prevents that
* across the page for the active drag and is reverted once the operation completes.
*/
private _toggleTextSelection(disabled: boolean): void {
const value = disabled ? 'none' : '';
const { style } = this._host.ownerDocument.body;

style.userSelect = value;
style.webkitUserSelect = value;

if (disabled) {
this._host.ownerDocument.getSelection()?.removeAllRanges();
}
}
Comment on lines +378 to +388

private _updateMatcher(event: PointerEvent) {
if (!this._options.matchTarget) {
return;
Expand Down
66 changes: 66 additions & 0 deletions src/components/tile-manager/tile-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,72 @@ describe('Tile Manager component', () => {
});
});

describe('Maximize', () => {
beforeEach(async () => {
tileManager = await fixture<IgcTileManagerComponent>(createTileManager());
});

it('issue 2029 - preserves grid container height when maximizing the only tile with the maximum row-span', async () => {
const grid = getTileManagerBase();
const tile = tileManager.tiles[0];

// Make the first tile the sole contributor to the tallest row track.
tile.rowSpan = 30;
await elementUpdated(tileManager);

const initialHeight = grid.offsetHeight;
expect(grid.style.minHeight).to.equal('');

tile.maximized = true;
await elementUpdated(tileManager);

// The grid height is locked so the maximized tile's content is not clipped.
expect(grid.style.minHeight).to.equal(`${initialHeight}px`);
expect(grid.offsetHeight).to.equal(initialHeight);
});

it('issue 2029 - releases the locked grid height once no tile is maximized', async () => {
const grid = getTileManagerBase();
const tile = tileManager.tiles[0];

tile.rowSpan = 30;
await elementUpdated(tileManager);

tile.maximized = true;
await elementUpdated(tileManager);
expect(grid.style.minHeight).to.not.equal('');

tile.maximized = false;
await elementUpdated(tileManager);
expect(grid.style.minHeight).to.equal('');
});

it('issue 2029 - keeps the grid height locked while any tile remains maximized', async () => {
const grid = getTileManagerBase();
const [firstTile, secondTile] = tileManager.tiles;

firstTile.maximized = true;
await elementUpdated(tileManager);

const lockedHeight = grid.style.minHeight;
expect(lockedHeight).to.not.equal('');

secondTile.maximized = true;
await elementUpdated(tileManager);

// The lock is retained (and not re-measured) while another tile is still maximized.
expect(grid.style.minHeight).to.equal(lockedHeight);

firstTile.maximized = false;
await elementUpdated(tileManager);
expect(grid.style.minHeight).to.equal(lockedHeight);

secondTile.maximized = false;
await elementUpdated(tileManager);
expect(grid.style.minHeight).to.equal('');
});
});

describe('Manual slot assignment', () => {
beforeEach(async () => {
tileManager = await fixture<IgcTileManagerComponent>(html`
Expand Down
27 changes: 27 additions & 0 deletions src/components/tile-manager/tile-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default class IgcTileManagerComponent extends LitElement {
return {
instance: this,
grid: this._grid,
setMaximizedState: () => this._setMaximizedState(),
};
}

Expand Down Expand Up @@ -246,6 +247,32 @@ export default class IgcTileManagerComponent extends LitElement {
this._tilesState.assignTiles();
}

/**
* Locks/unlocks the grid container height in response to a tile's maximized state changing.
*
* @remarks
* Maximizing a tile removes it from the grid flow (absolute positioning), so it no longer
* contributes to the grid's intrinsic height. When that tile is the sole contributor to the
* tallest row track, the container would otherwise collapse to the remaining tiles and cut off
* the maximized tile's content. Capturing the current height before the layout change keeps the
* container stable, and it is released once no tile remains maximized.
*/
private _setMaximizedState(): void {
const grid = this._grid.value;

if (grid) {
if (this.tiles.some((tile) => tile.maximized)) {
if (!grid.style.minHeight) {
grid.style.minHeight = `${grid.offsetHeight}px`;
}
} else {
grid.style.minHeight = '';
}
}

this.requestUpdate();
}
Comment on lines +260 to +274

// #endregion

// #region Public API
Expand Down
2 changes: 1 addition & 1 deletion src/components/tile-manager/tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ export default class IgcTileComponent extends EventEmitterMixin<
this._maximized = value;

if (this._tileManagerCtx) {
this._tileManagerCtx.instance.requestUpdate();
this._tileManagerCtx.setMaximizedState();
}
}

Expand Down
Loading