diff --git a/community-modules/styles/src/internal/base/parts/_grid-layout.scss b/community-modules/styles/src/internal/base/parts/_grid-layout.scss index 0c2189b1b8e..0f54d2d1fd9 100644 --- a/community-modules/styles/src/internal/base/parts/_grid-layout.scss +++ b/community-modules/styles/src/internal/base/parts/_grid-layout.scss @@ -119,19 +119,26 @@ --ag-internal-row-overlay-image: none; } - .ag-row .ag-grid-container-wrapper { + .ag-grid-container-wrapper { width: 100%; height: 100%; - background-color: inherit; } .ag-row:not(.ag-header-row) { + // Pinned rows need an opaque background, but the row background might be semi-transparent (very common + // for --ag-odd-row-background-color) so we use inheritance to pass the background down from the row > .ag-grid-pinned-left-cells, - > .ag-grid-pinned-right-cells, - > .ag-grid-scrolling-cells { + > .ag-grid-pinned-right-cells { background-color: inherit; + + // apply an opaque background background-image: linear-gradient(var(--ag-data-background-color), var(--ag-data-background-color)); } + + .ag-grid-container-wrapper { + // re-apply the transparent row color over the opaque background + background-color: inherit; + } } .ag-spanned-cell-wrapper { diff --git a/community-modules/styles/src/internal/base/parts/_root.scss b/community-modules/styles/src/internal/base/parts/_root.scss index e0c88d8cb49..f548482ca01 100644 --- a/community-modules/styles/src/internal/base/parts/_root.scss +++ b/community-modules/styles/src/internal/base/parts/_root.scss @@ -9,6 +9,10 @@ --ag-indentation-level: 0; } + .ag-styled-root { + display: contents; + } + [class*='ag-theme-'] { -webkit-font-smoothing: antialiased; font-family: var(--ag-font-family); diff --git a/community-modules/styles/src/internal/base/parts/_sidebar.scss b/community-modules/styles/src/internal/base/parts/_sidebar.scss index 211941cb3a9..0928abf8f64 100644 --- a/community-modules/styles/src/internal/base/parts/_sidebar.scss +++ b/community-modules/styles/src/internal/base/parts/_sidebar.scss @@ -29,6 +29,8 @@ } .ag-tool-panel-external { + width: 100%; + height: 100%; display: flex; flex-direction: row; } diff --git a/community-modules/styles/src/internal/themes/alpine/_index.scss b/community-modules/styles/src/internal/themes/alpine/_index.scss index 8a1a1220258..e3b87eab5b8 100644 --- a/community-modules/styles/src/internal/themes/alpine/_index.scss +++ b/community-modules/styles/src/internal/themes/alpine/_index.scss @@ -122,7 +122,7 @@ margin-bottom: 0; } - &.ag-dnd-ghost { + .ag-dnd-ghost { font-size: calc(var(--ag-font-size) - 1px); font-weight: 700; } diff --git a/community-modules/styles/src/internal/themes/balham/_index.scss b/community-modules/styles/src/internal/themes/balham/_index.scss index 1059afcc029..9b7b4cf22ce 100644 --- a/community-modules/styles/src/internal/themes/balham/_index.scss +++ b/community-modules/styles/src/internal/themes/balham/_index.scss @@ -29,7 +29,7 @@ color: var(--ag-disabled-foreground-color); } - &.ag-dnd-ghost { + .ag-dnd-ghost { font-size: var(--ag-font-size); font-weight: 600; } diff --git a/community-modules/styles/src/internal/themes/material/_index.scss b/community-modules/styles/src/internal/themes/material/_index.scss index 690aa2af12f..fbde55725bc 100644 --- a/community-modules/styles/src/internal/themes/material/_index.scss +++ b/community-modules/styles/src/internal/themes/material/_index.scss @@ -232,7 +232,7 @@ } } - &.ag-dnd-ghost { + .ag-dnd-ghost { font-size: calc(var(--ag-font-size) - 1px); font-weight: 600; } diff --git a/community-modules/styles/src/internal/themes/quartz/_index.scss b/community-modules/styles/src/internal/themes/quartz/_index.scss index 890b4d28059..6c575bd8e68 100644 --- a/community-modules/styles/src/internal/themes/quartz/_index.scss +++ b/community-modules/styles/src/internal/themes/quartz/_index.scss @@ -362,7 +362,7 @@ border: var(--ag-borders-secondary) var(--ag-secondary-border-color); } - &.ag-dnd-ghost { + .ag-dnd-ghost { font-weight: 500; } diff --git a/documentation/ag-grid-docs/src/components/theme-builder-homepage/ThemeBuilderHomepage.tsx b/documentation/ag-grid-docs/src/components/theme-builder-homepage/ThemeBuilderHomepage.tsx index c703e9f290c..95a8788acb5 100644 --- a/documentation/ag-grid-docs/src/components/theme-builder-homepage/ThemeBuilderHomepage.tsx +++ b/documentation/ag-grid-docs/src/components/theme-builder-homepage/ThemeBuilderHomepage.tsx @@ -186,7 +186,11 @@ export const ThemeBuilderHomepage: React.FC = ({ gridHeight = null }) => className={`${styles.grid} ${gridHeight ? '' : styles.gridHeight}`} > -
+
{ nonce: undefined, moduleCss: undefined, }); - setThemeClass(theme._getCssClass()); + setThemeClass(theme._getCssClasses()[1]); // [1] is the theme apply classes eg ag-theme-params-1 style.textContent = theme._getParamsCss(); } diff --git a/documentation/ag-grid-docs/src/content/docs/styling-tutorial/index.mdoc b/documentation/ag-grid-docs/src/content/docs/styling-tutorial/index.mdoc index 458a66e8f15..29a13475f33 100644 --- a/documentation/ag-grid-docs/src/content/docs/styling-tutorial/index.mdoc +++ b/documentation/ag-grid-docs/src/content/docs/styling-tutorial/index.mdoc @@ -511,7 +511,7 @@ export default { {% /if %} -The active colour scheme can then be controlled by setting the `data-ag-theme-mode="mode"` attribute on any parent element of the grid, commonly the `html` or `body` elements: +The active colour scheme is controlled by setting `data-ag-theme-mode="mode"` on the `` or `` element. {% if isFramework("javascript") %} diff --git a/documentation/ag-grid-docs/src/content/docs/theming-colors/index.mdoc b/documentation/ag-grid-docs/src/content/docs/theming-colors/index.mdoc index b6141856535..3b3e496fc9a 100644 --- a/documentation/ag-grid-docs/src/content/docs/theming-colors/index.mdoc +++ b/documentation/ag-grid-docs/src/content/docs/theming-colors/index.mdoc @@ -100,12 +100,23 @@ The standard way of changing a grid's appearance after initialisation is to upda Often however, a grid application is embedded within a website, and the website and grid application have different codebases. It may not be easy to update the theme grid option in response to the website's dark mode changing. -For this use case we provide theme modes. When a theme uses the `colorSchemeVariable` colour scheme, which is the default for our [built-in themes](./themes/#built-in-themes), the colour scheme can be controlled by setting the `data-ag-theme-mode="mode"` attribute on any parent element of the grid, commonly the `html` or `body` elements, where `mode` is any of: +For this use case we provide theme modes. When a theme uses the `colorSchemeVariable` colour scheme, which is the default for our [built-in themes](./themes/#built-in-themes), the colour scheme can be controlled by setting the `data-ag-theme-mode="mode"` attribute on the `` or `` elements, where `mode` is one of: - `light` - `dark` - `dark-blue` +{% note %} +If your grid is inside Shadow DOM or you only want to change the mode of some grids on the page, you may set the attribute on any ancestor element of the grid that has the `ag-theme-mode` class on it: + +```html +
+ ... +
+``` + +{% /note %} + You can also define custom colour modes by passing the mode name as the second argument to `withParams`. This example defines custom colour schemes for light and dark mode and switches between them by setting the `data-ag-theme-mode` attribute on the `body` element: ```js diff --git a/documentation/ag-grid-docs/src/content/docs/theming-test/_examples/theming-api/main.ts b/documentation/ag-grid-docs/src/content/docs/theming-test/_examples/theming-api/main.ts index 873669b0c6e..2af1bfc19a7 100644 --- a/documentation/ag-grid-docs/src/content/docs/theming-test/_examples/theming-api/main.ts +++ b/documentation/ag-grid-docs/src/content/docs/theming-test/_examples/theming-api/main.ts @@ -45,11 +45,10 @@ function useTheme(theme: string, isDark: boolean) { themePart = themeMaterial; break; } - const gridDiv = document.getElementById('myGrid')!; if (isDark) { - gridDiv.setAttribute('data-ag-theme-mode', 'dark'); + document.body.setAttribute('data-ag-theme-mode', 'dark'); } else { - gridDiv.removeAttribute('data-ag-theme-mode'); + document.body.removeAttribute('data-ag-theme-mode'); } gridApi.setGridOption('theme', themePart); } diff --git a/packages/ag-grid-angular/projects/ag-grid-angular/src/lib/ag-grid-angular.component.ts b/packages/ag-grid-angular/projects/ag-grid-angular/src/lib/ag-grid-angular.component.ts index 7fd86c1ad9f..09ffe72b364 100644 --- a/packages/ag-grid-angular/projects/ag-grid-angular/src/lib/ag-grid-angular.component.ts +++ b/packages/ag-grid-angular/projects/ag-grid-angular/src/lib/ag-grid-angular.component.ts @@ -332,7 +332,6 @@ export class AgGridAngular = ColDef extends IComponent, IDragAndDropImage {} -// the wrapper div has no class - the drag and drop service adds the theme class to it const DragAndDropElement: ElementParams = { tag: 'div', + cls: 'ag-dnd-ghost ag-unselectable', children: [ - { - tag: 'div', - ref: 'eGhost', - cls: 'ag-dnd-ghost ag-unselectable', - children: [ - { tag: 'span', ref: 'eIcon', cls: 'ag-dnd-ghost-icon ag-shake-left-to-right' }, - { tag: 'div', ref: 'eLabel', cls: 'ag-dnd-ghost-label' }, - ], - }, + { tag: 'span', ref: 'eIcon', cls: 'ag-dnd-ghost-icon ag-shake-left-to-right' }, + { tag: 'div', ref: 'eLabel', cls: 'ag-dnd-ghost-label' }, ], }; export class DragAndDropImageComponent extends Component implements IDragAndDropImageComponent { @@ -41,7 +34,6 @@ export class DragAndDropImageComponent extends Component implements IDragAndDrop private readonly eIcon: HTMLElement = RefPlaceholder; private readonly eLabel: HTMLElement = RefPlaceholder; - private readonly eGhost: HTMLElement = RefPlaceholder; private dropIconMap: { [key in DragAndDropIcon]: Element }; @@ -67,12 +59,7 @@ export class DragAndDropImageComponent extends Component implements IDragAndDrop public init(params: IDragAndDropImageParams): void { this.dragSource = params.dragSource; - this.setTemplate(DragAndDropElement); - // also apply theme class to the ghost element for backwards compatibility - // with themes that use .ag-theme-classname.ag-dnd-ghost, which used to be - // required before the theme class was also set on the wrapper. - this.beans.environment.applyThemeClasses(this.eGhost); } public override destroy(): void { @@ -81,7 +68,8 @@ export class DragAndDropImageComponent extends Component implements IDragAndDrop } public setIcon(iconName: DragAndDropIcon | null, shake: boolean): void { - const { eGhost, eIcon, dragSource, dropIconMap, gos } = this; + const { eIcon, dragSource, dropIconMap, gos } = this; + const eGhost = this.getGui(); _clearElement(eIcon); diff --git a/packages/ag-grid-community/src/grid.ts b/packages/ag-grid-community/src/grid.ts index 1aca15cbee7..8758d0a99c9 100644 --- a/packages/ag-grid-community/src/grid.ts +++ b/packages/ag-grid-community/src/grid.ts @@ -1,5 +1,5 @@ import type { AgContextParams } from 'ag-stack'; -import { AgContext, _missing } from 'ag-stack'; +import { AgContext, _createStyledRootElements, _missing } from 'ag-stack'; import { createGridApi } from './api/apiUtils'; import type { GridApi } from './api/gridApi'; @@ -32,7 +32,6 @@ import { _registerModule, _unRegisterGridModules, } from './modules/moduleRegistry'; -import { _createElement } from './utils/element'; import { NoModulesRegisteredError, missingRowModelTypeError } from './validation/errorMessages/errorText'; import { _error, _logPreInitErr } from './validation/logging'; import { VanillaFrameworkOverrides } from './vanillaFrameworkOverrides'; @@ -47,8 +46,6 @@ export interface GridParams { frameworkOverrides?: IFrameworkOverrides; // INTERNAL - bean instances to add to the context providedBeanInstances?: { [key: string]: any }; - // INTERNAL - set by frameworks if the provided grid div is safe to set a theme class on - setThemeOnGridDiv?: boolean; // INTERNAL - set by studio withinStudio?: boolean; @@ -86,29 +83,19 @@ export function createGrid( _error(11); return {} as GridApi; } - const gridParams: GridParams | undefined = params; - let destroyCallback: (() => void) | undefined; - if (!gridParams?.setThemeOnGridDiv) { - // frameworks already create an element owned by our code, so we can set - // the theme class on it. JS users calling createGrid directly are - // passing an element owned by their application, so we can't set a - // class name on it and must create a wrapper. - const newGridDiv = _createElement({ tag: 'div' }); - newGridDiv.style.height = '100%'; - eGridDiv.appendChild(newGridDiv); - eGridDiv = newGridDiv; - destroyCallback = () => eGridDiv.remove(); - } + const [outer, inner] = _createStyledRootElements(); + eGridDiv.appendChild(outer); const api = new GridCoreCreator().create( - eGridDiv, + outer, + inner, gridOptions, (context) => { - const gridComp = new GridComp(eGridDiv); + const gridComp = new GridComp(inner); context.createBean(gridComp); }, undefined, params, - destroyCallback + () => outer.remove() ); return api; @@ -120,7 +107,12 @@ let nextGridId = 1; // their own UI /** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */ export class GridCoreCreator { + /** + * @param eOutermostGridOwned the outermost element owned by grid code, the parent of which is application-owned + * @param eGridDiv the element into which the grid UI should be appended - the inner element of the styled root + */ public create( + eOutermostGridOwned: HTMLElement, eGridDiv: HTMLElement, providedOptions: GridOptions, createUi: (context: Context) => void, @@ -151,7 +143,7 @@ export class GridCoreCreator { const destroyCallback = () => { _gridElementCache.delete(api); - _gridApiCache.delete(eGridDiv); + _gridApiCache.delete(eOutermostGridOwned); _unRegisterGridModules(gridId); _destroyCallback?.(); }; @@ -189,8 +181,8 @@ export class GridCoreCreator { const api = context.getBean('gridApi'); - _gridApiCache.set(eGridDiv, api); - _gridElementCache.set(api, eGridDiv); + _gridApiCache.set(eOutermostGridOwned, api); + _gridElementCache.set(api, eOutermostGridOwned); return api; } @@ -339,30 +331,33 @@ function getDefaultRowModelType(passedRowModelType?: RowModelType): RowModelType } /** - * Returns a `GridApi` instance that is associated with the grid rendered in `gridElement`. - * - * The `gridElement` argument can be one of the following: - * - a DOM node - * - the grid ID as determined by the `gridId` grid option. - * - CSS selector string + * Returns the `GridApi` associated with a grid * - * When using a CSS selector, it must refer to the element passed to `createGrid`. - * - * If passing a DOM node as an argument, this DOM node must be an immediate child of the element passed - * to `createGrid`. This is to support the case where multiple grids are instantiated in a single element. + * The `gridElement` argument can be: + * - the grid ID as determined by the `gridId` grid option + * - a DOM node or a CSS selector string identifying a DOM node. This can point + * to any element within a grid, or to the parent element of the grid if the + * grid is the first child. */ export function getGridApi(gridElement: Element | string | null | undefined): GridApi | undefined { if (typeof gridElement === 'string') { try { gridElement = - document.querySelector(`[grid-id="${gridElement}"]`)?.parentElement ?? - document.querySelector(gridElement)?.firstElementChild ?? - document.getElementById(gridElement)?.firstElementChild; + document.querySelector(`[grid-id="${gridElement}"]`) ?? + document.querySelector(gridElement) ?? + document.getElementById(gridElement); } catch { gridElement = null; } } - return gridElement ? _gridApiCache.get(gridElement) : undefined; + gridElement = gridElement?.firstElementChild ?? gridElement; + while (gridElement) { + const api = _gridApiCache.get(gridElement); + if (api) { + return api; + } + gridElement = gridElement.parentElement; + } } /** diff --git a/packages/ag-grid-community/src/gridComp/gridComp.ts b/packages/ag-grid-community/src/gridComp/gridComp.ts index 5048811bbd0..0234484dc7d 100644 --- a/packages/ag-grid-community/src/gridComp/gridComp.ts +++ b/packages/ag-grid-community/src/gridComp/gridComp.ts @@ -36,7 +36,6 @@ export class GridComp extends TabGuardComp { public postConstruct(): void { const compProxy: IGridComp = { destroyGridUi: () => this.destroyBean(this), - setRtlClass: (cssClass: string) => this.addCss(cssClass), forceFocusOutOfContainer: this.forceFocusOutOfContainer.bind(this), updateLayoutClasses: this.updateLayoutClasses.bind(this), getFocusableContainers: this.getFocusableContainers.bind(this), diff --git a/packages/ag-grid-community/src/gridComp/gridCtrl.ts b/packages/ag-grid-community/src/gridComp/gridCtrl.ts index c4774e13730..2a4b1485060 100644 --- a/packages/ag-grid-community/src/gridComp/gridCtrl.ts +++ b/packages/ag-grid-community/src/gridComp/gridCtrl.ts @@ -12,7 +12,6 @@ import type { Component, ComponentSelector } from '../widgets/component'; /** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */ export interface IGridComp extends LayoutView { - setRtlClass(cssClass: string): void; destroyGridUi(): void; forceFocusOutOfContainer(up: boolean): void; getFocusableContainers(): FocusableContainer[]; @@ -66,8 +65,6 @@ export class GridCtrl extends BeanStub { this.createManagedBean(new LayoutFeature(this.view)); - this.view.setRtlClass(this.gos.get('enableRtl') ? 'ag-rtl' : 'ag-ltr'); - if (this.gos.get('suppressContentVisibilityAuto')) { this.eGui.style.setProperty('content-visibility', 'visible'); } diff --git a/packages/ag-grid-community/src/rendering/row/rowComp.ts b/packages/ag-grid-community/src/rendering/row/rowComp.ts index 59663d1d90c..8f2b607d94b 100644 --- a/packages/ag-grid-community/src/rendering/row/rowComp.ts +++ b/packages/ag-grid-community/src/rendering/row/rowComp.ts @@ -15,16 +15,19 @@ import type { IRowComp, RowCtrl } from './rowCtrl'; const LEAF_RENDERER_TAGS = new Set(['CANVAS', 'IMG', 'SVG', 'VIDEO', 'AUDIO', 'INPUT', 'IFRAME', 'PICTURE']); -const createCellSection = (sectionClass: string): { container: HTMLElement; wrapper: HTMLElement } => { - const wrapper = _createElement({ +const createCellSection = (sectionClass: string, pinned: boolean): { container: HTMLElement; wrapper: HTMLElement } => { + const container = _createElement({ tag: 'div', + cls: sectionClass, role: 'presentation', - cls: 'ag-grid-container-wrapper', }); - const container = _createElement({ + if (!pinned) { + return { container, wrapper: container }; + } + const wrapper = _createElement({ tag: 'div', - cls: sectionClass, role: 'presentation', + cls: 'ag-grid-container-wrapper', }); container.appendChild(wrapper); return { container, wrapper }; @@ -55,9 +58,9 @@ export class RowComp extends Component { const rowDiv = _createElement({ tag: 'div', role: 'row', attrs: { 'comp-id': `${this.getCompId()}` } }); if (shouldCreateCellSections) { - const leftSection = createCellSection('ag-grid-pinned-left-cells'); - const centerSection = createCellSection('ag-grid-scrolling-cells'); - const rightSection = createCellSection('ag-grid-pinned-right-cells'); + const leftSection = createCellSection('ag-grid-pinned-left-cells', true); + const centerSection = createCellSection('ag-grid-scrolling-cells', false); + const rightSection = createCellSection('ag-grid-pinned-right-cells', true); this.ePinnedLeftSection = leftSection.container; this.ePinnedLeftCells = leftSection.wrapper; @@ -65,7 +68,8 @@ export class RowComp extends Component { this.ePinnedRightSection = rightSection.container; this.ePinnedRightCells = rightSection.wrapper; - rowDiv.append(leftSection.container, centerSection.container, rightSection.container); + // The centre lane is always present; the pinned lanes are attached on demand. + rowDiv.append(centerSection.container); } this.setInitialStyle(rowDiv); this.setTemplateFromElement(rowDiv); @@ -77,10 +81,9 @@ export class RowComp extends Component { setDomOrder: (domOrder) => (this.domOrder = domOrder), setCellCtrls: (cellCtrls) => this.setCellCtrls(cellCtrls), getPinnedLeftRowElement: () => this.ePinnedLeftCells, - getPinnedLeftSectionElement: () => this.ePinnedLeftSection, getScrollingRowElement: () => this.eScrollingCells, getPinnedRightRowElement: () => this.ePinnedRightCells, - getPinnedRightSectionElement: () => this.ePinnedRightSection, + refreshPinnedSections: () => this.refreshPinnedSections(), showFullWidth: (compDetails) => this.showFullWidth(compDetails), showEmbeddedFullWidth: (compDetails) => this.showEmbeddedFullWidth(compDetails), getFullWidthCellRenderers: () => this.getAllFullWidthCellRenderers(), @@ -108,6 +111,40 @@ export class RowComp extends Component { }); } + private refreshPinnedSections(): void { + const widths = this.rowCtrl.getMappedPinnedCellGroupWidths(); + const eCenter = this.eScrollingCells; + if (eCenter) { + eCenter.style.width = `${widths.centerWidth}px`; + } + + const isFullWidth = this.rowCtrl.isFullWidth(); + + const refreshPinnedSection = (eSection: HTMLElement | undefined, width: number, method: 'after' | 'before') => { + if (!eSection) { + return; + } + if ( + // Skip rendering pinned cell containers when there are no pinned + // columns to improve rendering performance + width <= 0 && + // Render always for full width rows, because the row renderer + // requires a reference to these even if they are empty + !isFullWidth + ) { + eSection.remove(); + return; + } + eSection.style.width = `${width}px`; + if (!eSection.parentNode && eCenter) { + eCenter[method](eSection); + } + }; + + refreshPinnedSection(this.ePinnedLeftSection, widths.leftWidth, 'before'); + refreshPinnedSection(this.ePinnedRightSection, widths.rightWidth, 'after'); + } + private setInitialStyle(container: HTMLElement): void { const transform = this.rowCtrl.getInitialTransform(); diff --git a/packages/ag-grid-community/src/rendering/row/rowCtrl.ts b/packages/ag-grid-community/src/rendering/row/rowCtrl.ts index d0a4c6671e0..ff999726216 100644 --- a/packages/ag-grid-community/src/rendering/row/rowCtrl.ts +++ b/packages/ag-grid-community/src/rendering/row/rowCtrl.ts @@ -71,10 +71,9 @@ export interface IRowComp { toggleCss(cssClassName: string, on: boolean): void; setCellCtrls(cellCtrls: CellCtrl[], useFlushSync: boolean): void; getPinnedLeftRowElement(): HTMLElement | undefined; - getPinnedLeftSectionElement(): HTMLElement | undefined; getScrollingRowElement(): HTMLElement | undefined; getPinnedRightRowElement(): HTMLElement | undefined; - getPinnedRightSectionElement(): HTMLElement | undefined; + refreshPinnedSections(): void; showFullWidth(compDetails: UserCompDetails): void; showEmbeddedFullWidth?(compDetails: HorizontalSectionMap): void; getFullWidthCellRenderers(): (ICellRenderer | null | undefined)[]; @@ -471,24 +470,7 @@ export class RowCtrl extends BeanStub { if (!rowGui) { return; } - const { rowComp } = rowGui; - const widths = this.getMappedPinnedCellGroupWidths(); - - this.setPinnedSectionWidth( - rowComp.getPinnedLeftRowElement(), - rowComp.getPinnedLeftSectionElement(), - widths.leftWidth - ); - this.setPinnedSectionWidth( - rowComp.getPinnedRightRowElement(), - rowComp.getPinnedRightSectionElement(), - widths.rightWidth - ); - - const eScrolling = rowComp.getScrollingRowElement(); - if (eScrolling) { - eScrolling.style.width = `${widths.centerWidth}px`; - } + rowGui.rowComp.refreshPinnedSections(); } public getMappedPinnedCellGroupWidths(): PinnedCellGroupWidths { @@ -509,29 +491,6 @@ export class RowCtrl extends BeanStub { }; } - private setPinnedSectionWidth( - wrapper: HTMLElement | undefined, - section: HTMLElement | undefined, - width: number - ): void { - if (!wrapper) { - return; - } - const display = width > 0 ? '' : 'none'; - const widthPx = `${width}px`; - - const setStyles = (e: HTMLElement) => { - e.style.width = widthPx; - e.style.display = display; - }; - - setStyles(wrapper); - - if (section) { - setStyles(section); - } - } - public getPinnedCellGroupWidths(): PinnedCellGroupWidths { return getPinnedSectionWidths(this.beans.visibleCols, this.printLayout); } diff --git a/packages/ag-grid-community/src/rendering/rowRenderer.ts b/packages/ag-grid-community/src/rendering/rowRenderer.ts index 7739af66dbb..3e4dde4fb08 100644 --- a/packages/ag-grid-community/src/rendering/rowRenderer.ts +++ b/packages/ag-grid-community/src/rendering/rowRenderer.ts @@ -1055,9 +1055,9 @@ export class RowRenderer extends BeanStub implements NamedBean { // 1) height of grid body changes, ie number of displayed rows has changed // 2) grid scrolled to new position // 3) ensure index visible (which is a scroll) - public redraw(params: { afterScroll?: boolean } = {}) { + public redraw(params: { afterScroll?: boolean; force?: boolean } = {}) { const { focusSvc, animationFrameSvc } = this.beans; - const { afterScroll } = params; + const { afterScroll, force } = params; let cellFocused: CellPosition | undefined; const stickyRowFeature = this.stickyRowFeature; @@ -1086,7 +1086,7 @@ export class RowRenderer extends BeanStub implements NamedBean { const rangeChanged = this.firstRenderedRow !== oldFirstRow || this.lastRenderedRow !== oldLastRow; - if (afterScroll && !hasStickyRowChanges && !rangeChanged) { + if (afterScroll && !hasStickyRowChanges && !rangeChanged && !force) { return; } @@ -1255,7 +1255,7 @@ export class RowRenderer extends BeanStub implements NamedBean { this.refreshPinnedRowComps(); this.removeRowCtrls(rowsToRemove); - this.redraw({ afterScroll: true }); + this.redraw({ afterScroll: true, force: true }); } public getFullWidthRowCtrls(rowNodes?: IRowNode[]): RowCtrl[] { diff --git a/packages/ag-grid-community/src/theming/core/css/_grid-layout.css b/packages/ag-grid-community/src/theming/core/css/_grid-layout.css index 9fa49b476b7..fb7dd6518a9 100644 --- a/packages/ag-grid-community/src/theming/core/css/_grid-layout.css +++ b/packages/ag-grid-community/src/theming/core/css/_grid-layout.css @@ -120,20 +120,26 @@ --ag-internal-row-overlay-image: none; } -/* rows need a solid background to avoid being transparent when dragging */ -.ag-row .ag-grid-container-wrapper { +.ag-grid-container-wrapper { width: 100%; height: 100%; - background-color: inherit; } .ag-row:not(.ag-header-row) { + /* Pinned rows need an opaque background, but the row background might be semi-transparent (very common + for --ag-odd-row-background-color) so we use inheritance to pass the background down from the row */ > .ag-grid-pinned-left-cells, - > .ag-grid-pinned-right-cells, - > .ag-grid-scrolling-cells { + > .ag-grid-pinned-right-cells { background-color: inherit; + + /* apply an opaque background */ background-image: linear-gradient(var(--ag-data-background-color), var(--ag-data-background-color)); } + + .ag-grid-container-wrapper { + /* re-apply the transparent row color over the opaque background */ + background-color: inherit; + } } .ag-body-vertical-content-no-gap .ag-has-bottom-pinned-rows { diff --git a/packages/ag-grid-community/src/theming/core/css/_root.css b/packages/ag-grid-community/src/theming/core/css/_root.css index addca2769af..a0c03a11e08 100644 --- a/packages/ag-grid-community/src/theming/core/css/_root.css +++ b/packages/ag-grid-community/src/theming/core/css/_root.css @@ -11,6 +11,7 @@ ag-grid-angular { overflow: hidden; border: var(--ag-wrapper-border); border-radius: var(--ag-wrapper-border-radius); + background-color: var(--ag-wrapper-background-color); &.ag-layout-normal { content-visibility: auto; diff --git a/packages/ag-grid-community/src/theming/parts/input-style/input-style-base.css b/packages/ag-grid-community/src/theming/parts/input-style/input-style-base.css index c00b84124c8..f41d57f87c5 100644 --- a/packages/ag-grid-community/src/theming/parts/input-style/input-style-base.css +++ b/packages/ag-grid-community/src/theming/parts/input-style/input-style-base.css @@ -60,21 +60,6 @@ } } -/* stylelint-disable-next-line nesting-selector-no-missing-scoping-root -- RTI-3243 auto rtl doesn't - work for elements outside the grid, tactical fix until AG-16725 */ -&:where(.ag-ltr, .ag-rtl) - .ag-input-field-input:where( - input:not([type]), - input[type='text'], - input[type='number'], - input[type='tel'], - input[type='date'], - input[type='datetime-local'], - textarea - ) { - padding: 0 var(--ag-input-padding-start); -} - /* icon for search inputs */ :where(.ag-column-select-header-filter-wrapper), :where(.ag-filter-toolpanel-search), diff --git a/packages/ag-grid-enterprise/src/advancedFilter/advancedFilterCtrl.ts b/packages/ag-grid-enterprise/src/advancedFilter/advancedFilterCtrl.ts index aef965b339c..120d9bb1c61 100644 --- a/packages/ag-grid-enterprise/src/advancedFilter/advancedFilterCtrl.ts +++ b/packages/ag-grid-enterprise/src/advancedFilter/advancedFilterCtrl.ts @@ -1,4 +1,4 @@ -import { _getAbsoluteHeight, _getAbsoluteWidth, _removeFromParent } from 'ag-stack'; +import { _getAbsoluteHeight, _getAbsoluteWidth, _initStyledRoot } from 'ag-stack'; import type { BeanCollection, @@ -33,6 +33,7 @@ export class AdvancedFilterCtrl extends BeanStub implem private eHeaderComp: AdvancedFilterHeaderComp | undefined; private headerCompHost: IPinnedSectionCompHost | undefined; private eFilterComp: AdvancedFilterComp | undefined; + private disconnectFilterComp: (() => void) | undefined; private hasAdvancedFilterParent: boolean; private eBuilderComp: AdvancedFilterBuilderComp | undefined; private eBuilderDialog: Dialog | undefined; @@ -59,7 +60,7 @@ export class AdvancedFilterCtrl extends BeanStub implem }); this.addDestroyFunc(() => { - this.destroyAdvancedFilterComp(); + this.destroyFilterComp(); if (this.eHeaderComp) { this.headerCompHost?.unmountComp(this.eHeaderComp.getGui()); this.destroyBean(this.eHeaderComp); @@ -195,7 +196,7 @@ export class AdvancedFilterCtrl extends BeanStub implem } private setAdvancedFilterComp(): void { - this.destroyAdvancedFilterComp(); + this.destroyFilterComp(); if (!this.enabled) { return; } @@ -207,11 +208,7 @@ export class AdvancedFilterCtrl extends BeanStub implem const eAdvancedFilterComp = this.createBean(new AdvancedFilterComp()); const eAdvancedFilterCompGui = eAdvancedFilterComp.getGui(); - this.environment.applyThemeClasses(eAdvancedFilterCompGui); - - eAdvancedFilterCompGui.classList.add(this.gos.get('enableRtl') ? 'ag-rtl' : 'ag-ltr'); - - advancedFilterParent.appendChild(eAdvancedFilterCompGui); + this.disconnectFilterComp = _initStyledRoot(this.environment, advancedFilterParent, eAdvancedFilterCompGui); this.eFilterComp = eAdvancedFilterComp; } @@ -242,10 +239,8 @@ export class AdvancedFilterCtrl extends BeanStub implem this.eHeaderComp.refreshLayout(); } - private destroyAdvancedFilterComp(): void { - if (this.eFilterComp) { - _removeFromParent(this.eFilterComp.getGui()); - this.destroyBean(this.eFilterComp); - } + private destroyFilterComp(): void { + this.disconnectFilterComp?.(); + this.destroyBean(this.eFilterComp); } } diff --git a/packages/ag-grid-enterprise/src/agStack/agContextMenuService.ts b/packages/ag-grid-enterprise/src/agStack/agContextMenuService.ts index 48444b32d90..227aaa038fd 100644 --- a/packages/ag-grid-enterprise/src/agStack/agContextMenuService.ts +++ b/packages/ag-grid-enterprise/src/agStack/agContextMenuService.ts @@ -14,6 +14,7 @@ import { _focusInto, _getPageBody, _getRootNode, + _initStyledRoot, _isPromise, _isVisible, } from 'ag-stack'; @@ -154,12 +155,11 @@ export class AgContextMenuService< return; } - targetEl.appendChild(wrapperEl); + const styledRootDisconnect = _initStyledRoot(beans.environment, targetEl, wrapperEl); beans.ariaAnnounce?.announceValue( translate('ariaLabelLoadingContextMenu', 'Loading Context Menu'), 'contextmenu' ); - beans.environment.applyThemeClasses(wrapperEl); _anchorElementToMouseMoveEvent(wrapperEl, mouseEvent, beans); const mouseMoveCallback = (e: MouseEvent) => { @@ -170,7 +170,7 @@ export class AgContextMenuService< this.destroyLoadingSpinner = () => { rootNode.removeEventListener('mousemove', mouseMoveCallback); - wrapperEl.remove(); + styledRootDisconnect(); this.destroyLoadingSpinner = null; }; } diff --git a/packages/ag-grid-enterprise/src/charts/chartComp/gridChartComp.ts b/packages/ag-grid-enterprise/src/charts/chartComp/gridChartComp.ts index 7e37a356601..a15f05dea92 100644 --- a/packages/ag-grid-enterprise/src/charts/chartComp/gridChartComp.ts +++ b/packages/ag-grid-enterprise/src/charts/chartComp/gridChartComp.ts @@ -19,7 +19,6 @@ import type { ChartModel, ChartToolPanelName, ChartType, - Environment, FocusService, IAggFunc, PartialCellRange, @@ -84,7 +83,6 @@ export class GridChartComp extends Component { private focusSvc: FocusService; private popupSvc: PopupService; private enterpriseChartProxyFactory?: EnterpriseChartProxyFactory; - private environment: Environment; public wireBeans(beans: BeanCollection): void { this.crossFilterService = beans.chartCrossFilterSvc as ChartCrossFilterService; @@ -93,7 +91,6 @@ export class GridChartComp extends Component { this.focusSvc = beans.focusSvc; this.popupSvc = beans.popupSvc!; this.enterpriseChartProxyFactory = beans.enterpriseChartProxyFactory as EnterpriseChartProxyFactory; - this.environment = beans.environment; } private readonly eChart: HTMLElement = RefPlaceholder; @@ -140,10 +137,6 @@ export class GridChartComp extends Component { chartThemeName: this.getThemeName(), }; - const isRtl = this.gos.get('enableRtl'); - - this.eWrapper.classList.add(isRtl ? 'ag-rtl' : 'ag-ltr'); - // only the chart controller interacts with the chart model const model = this.createBean(new ChartDataModel(modelParams)); this.chartController = this.createManagedBean(new ChartController(model)); @@ -156,13 +149,6 @@ export class GridChartComp extends Component { if (this.params.insideDialog) { this.addDialog(); - } else { - // don't add the theme if we're in a dialog, since dialogs already - // add a theme, and legacy themes don't like being applied twice - this.addManagedEventListeners({ - stylesChanged: this.updateTheme.bind(this), - }); - this.updateTheme(); } this.addMenu(); @@ -179,10 +165,6 @@ export class GridChartComp extends Component { this.raiseChartCreatedEvent(); } - private updateTheme() { - this.environment.applyThemeClasses(this.getGui()); - } - private createChart(): void { // if chart already exists, destroy it and remove it from DOM let chartInstance: AgChartInstance | undefined = undefined; diff --git a/packages/ag-grid-enterprise/src/charts/chartService.ts b/packages/ag-grid-enterprise/src/charts/chartService.ts index d12b9427ed2..c524b60587d 100644 --- a/packages/ag-grid-enterprise/src/charts/chartService.ts +++ b/packages/ag-grid-enterprise/src/charts/chartService.ts @@ -1,5 +1,5 @@ import type { AgChartThemeOverrides, AgChartThemePalette } from 'ag-charts-types'; -import { _focusInto } from 'ag-stack'; +import { _focusInto, _initDetachedStyledRoot } from 'ag-stack'; import type { BaseCreateChartParams, @@ -270,12 +270,13 @@ export class ChartService extends BeanStub implements NamedBean, IChartService { const { chartType, chartContainer } = params; const createChartContainerFunc = this.gos.getCallback('createChartContainer'); + const insideDialog = !(chartContainer || createChartContainerFunc); const gridChartParams: GridChartParams = { ...params, chartId: this.generateId(), chartType: getCanonicalChartType(chartType), - insideDialog: !(chartContainer || createChartContainerFunc), + insideDialog, crossFilteringContext: this.crossFilteringContext, crossFilteringResetCallback: () => { for (const c of this.activeChartComps) { @@ -287,31 +288,25 @@ export class ChartService extends BeanStub implements NamedBean, IChartService { const chartComp = new GridChartComp(gridChartParams); this.createBean(chartComp); - const chartRef = this.createChartRef(chartComp); - - if (chartContainer) { - // if container exists, means developer initiated chart create via API, so place in provided container - chartContainer.appendChild(chartRef.chartElement); - } else if (createChartContainerFunc) { - // otherwise, user created chart via grid UI, check if developer provides containers (e.g. if the application - // is using its own dialogs rather than the grid provided dialogs) - createChartContainerFunc(chartRef); - } else { - // add listener to remove from active charts list when charts are destroyed, e.g. closing chart dialog - chartComp.addEventListener('destroyed', () => { - this.activeChartComps.delete(chartComp); - this.activeCharts.delete(chartRef); - }); + let chartElement = chartComp.getGui(); + let styledRootDestroy: (() => void) | undefined; + if (!insideDialog) { + // The chart is being created outside the grid so we need to create a styled root + [chartElement, styledRootDestroy] = _initDetachedStyledRoot(this.beans.environment, chartElement); + // If a container was supplied, append the chart (otherwise the chart will be passed to createChartContainerFunc) + chartContainer?.appendChild(chartElement); } - return chartRef; - } - - private createChartRef(chartComp: GridChartComp): ChartRef { const chartRef: ChartRef = { destroyChart: () => { if (this.activeCharts.has(chartRef)) { this.destroyBean(chartComp); + styledRootDestroy?.(); + if (chartContainer) { + // Only remove the chart if we added it (in the createChartContainerFunc case, + // the application inserted it and is responsible for removing it) + chartElement.remove(); + } this.activeChartComps.delete(chartComp); this.activeCharts.delete(chartRef); } @@ -319,7 +314,7 @@ export class ChartService extends BeanStub implements NamedBean, IChartService { focusChart: () => { _focusInto(chartComp.getGui()); }, - chartElement: chartComp.getGui(), + chartElement, chart: chartComp.getUnderlyingChart(), chartId: chartComp.getChartModel().chartId, setMaximized: chartComp.setMaximized.bind(chartComp), @@ -328,6 +323,16 @@ export class ChartService extends BeanStub implements NamedBean, IChartService { this.activeCharts.add(chartRef); this.activeChartComps.add(chartComp); + if (!chartContainer && createChartContainerFunc) { + createChartContainerFunc(chartRef); + } else if (!chartContainer) { + // add listener to remove from active charts list when charts are destroyed, e.g. closing chart dialog + chartComp.addEventListener('destroyed', () => { + this.activeChartComps.delete(chartComp); + this.activeCharts.delete(chartRef); + }); + } + return chartRef; } diff --git a/packages/ag-grid-enterprise/src/charts/css/_charts.css b/packages/ag-grid-enterprise/src/charts/css/_charts.css index 5714715082e..c081c904453 100644 --- a/packages/ag-grid-enterprise/src/charts/css/_charts.css +++ b/packages/ag-grid-enterprise/src/charts/css/_charts.css @@ -4,6 +4,7 @@ display: flex; width: 100%; height: 100%; + background-color: var(--ag-wrapper-background-color); } .ag-chart-components-wrapper { diff --git a/packages/ag-grid-enterprise/src/sideBar/agSideBar.css b/packages/ag-grid-enterprise/src/sideBar/agSideBar.css index 6a0def1dac9..93363469881 100644 --- a/packages/ag-grid-enterprise/src/sideBar/agSideBar.css +++ b/packages/ag-grid-enterprise/src/sideBar/agSideBar.css @@ -23,8 +23,11 @@ } .ag-tool-panel-external { + width: 100%; + height: 100%; display: flex; flex-direction: row; + background-color: var(--ag-wrapper-background-color); } :where(.ag-tool-panel-external) .ag-tool-panel-wrapper { diff --git a/packages/ag-grid-enterprise/src/sideBar/agSideBar.ts b/packages/ag-grid-enterprise/src/sideBar/agSideBar.ts index d7ddd4c4935..5a16c9f4132 100644 --- a/packages/ag-grid-enterprise/src/sideBar/agSideBar.ts +++ b/packages/ag-grid-enterprise/src/sideBar/agSideBar.ts @@ -362,12 +362,11 @@ class AgSideBar extends Component implements ISideBar, FocusableContainer { wrapper: ToolPanelWrapper, externalParent: HTMLElement | null | undefined ): void { - const wrapperGui = wrapper.getGui(); - if (externalParent) { - this.beans.environment.applyThemeClasses(externalParent, ['ag-external', 'ag-tool-panel-external']); - wrapperGui.classList.add(this.gos.get('enableRtl') ? 'ag-rtl' : 'ag-ltr'); - } const correctParent = externalParent ?? wrapper.getDefParent() ?? this.getGui(); + if (correctParent !== this.getGui()) { + wrapper.ensureStyledRoot(); + } + const wrapperGui = wrapper.getGui(); if (wrapperGui.parentElement !== correctParent) { correctParent.appendChild(wrapperGui); } diff --git a/packages/ag-grid-enterprise/src/sideBar/toolPanelWrapper.ts b/packages/ag-grid-enterprise/src/sideBar/toolPanelWrapper.ts index 52c3b1e7dee..49971739024 100644 --- a/packages/ag-grid-enterprise/src/sideBar/toolPanelWrapper.ts +++ b/packages/ag-grid-enterprise/src/sideBar/toolPanelWrapper.ts @@ -1,4 +1,4 @@ -import { RefPlaceholder } from 'ag-stack'; +import { RefPlaceholder, _createAgElement, _initDetachedStyledRoot } from 'ag-stack'; import type { ComponentType, @@ -47,6 +47,7 @@ export class ToolPanelWrapper extends Component { private params: IToolPanelParams; private animationId: number = 0; private defParent: HTMLElement | null = null; + private hasStyledRoot = false; constructor() { super(ToolPanelElement); @@ -62,6 +63,19 @@ export class ToolPanelWrapper extends Component { this.appendChild(resizeBar); } + public ensureStyledRoot(): void { + if (this.hasStyledRoot) { + return; + } + this.hasStyledRoot = true; + const innerGui = this.getGui(); + const externalDiv = _createAgElement({ tag: 'div', cls: 'ag-tool-panel-external' }); + externalDiv.appendChild(innerGui); + const [styledRootOuter, styledRootDestroy] = _initDetachedStyledRoot(this.beans.environment, externalDiv); + this.addDestroyFunc(styledRootDestroy); + this.setGui(styledRootOuter); + } + public getToolPanelId(): string { return this.toolPanelId; } diff --git a/packages/ag-grid-react/src/reactUi/agGridReactUi.tsx b/packages/ag-grid-react/src/reactUi/agGridReactUi.tsx index 14517c9d58a..c0eaa1415dc 100644 --- a/packages/ag-grid-react/src/reactUi/agGridReactUi.tsx +++ b/packages/ag-grid-react/src/reactUi/agGridReactUi.tsx @@ -59,7 +59,6 @@ import { LicenseContext, ModulesContext } from './agGridProvider'; import { BeansContext, RenderModeContext } from './beansContext'; import GridComp from './gridComp'; import { RenderStatusService } from './renderStatusService'; -import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; import { CssClasses, isReact19, runWithoutFlushSync } from './utils'; const deprecatedProps: Pick = { @@ -88,7 +87,7 @@ export const AgGridReactUi = (props: InternalAgGridReactProps) => const usesAgGridProvider = modulesFromContext !== null; const apiRef = useRef>(); - const eGui = useRef(null); + const innermostRef = useRef(null); const portalManager = useRef(null); const destroyFuncs = useRef<(() => void)[]>([]); const whenReadyFuncs = useRef<(() => void)[]>([]); @@ -103,36 +102,8 @@ export const AgGridReactUi = (props: InternalAgGridReactProps) => // Hook to enable Portals to be displayed via the PortalManager const [, setPortalRefresher] = useState(0); - const appliedClassName = useRef(); - const updateClassName = (classNameFromReact: string | undefined) => { - // Fix for AG-16224. The grid sets the className on the div using - // el.classList, so we must use classList too - if we did - // `className={props.className}` we would overwrite the grid's changes - const classList = eGui.current?.classList; - const splitClasses = (s = '') => s.trim().split(/\s+/g).filter(Boolean); - if (appliedClassName.current !== classNameFromReact) { - for (const cls of splitClasses(appliedClassName.current)) { - if (classList?.contains(cls)) { - classList.remove(cls); - } - } - for (const cls of splitClasses(classNameFromReact)) { - if (!classList?.contains(cls)) { - classList?.add(cls); - } - } - appliedClassName.current = classNameFromReact; - } - }; - - useIsomorphicLayoutEffect(() => { - updateClassName(props.className); - }, [props.className]); - - const setRef = useCallback((eRef: HTMLDivElement | null) => { - eGui.current = eRef; - updateClassName(props.className); - if (!eRef) { + const setOutermostRef = useCallback((outermost: HTMLDivElement | null) => { + if (!outermost) { ready.current = false; for (const f of destroyFuncs.current) { f(); @@ -188,7 +159,6 @@ export const AgGridReactUi = (props: InternalAgGridReactProps) => }, modules, frameworkOverrides, - setThemeOnGridDiv: true, }; const createUiCallback = (ctx: Context) => { @@ -243,7 +213,8 @@ export const AgGridReactUi = (props: InternalAgGridReactProps) => // We ensure that the gridId is stable even in StrictMode mergedGridOps.gridId ??= gridIdRef.current; apiRef.current = gridCoreCreator.create( - eRef, + outermost, + innermostRef.current!, mergedGridOps, createUiCallback, acceptChangesCallback, @@ -259,6 +230,7 @@ export const AgGridReactUi = (props: InternalAgGridReactProps) => const style = useMemo(() => { return { + width: '100%', height: '100%', ...(props.containerStyle || {}), }; @@ -287,13 +259,20 @@ export const AgGridReactUi = (props: InternalAgGridReactProps) => ? 'legacy' : 'default'; return ( - // IMPORTANT! Don't set className here, we must use classList - // imperatively to avoid removing classes set by the grid -
- - {context && !context.isDestroyed() ? : null} - {portalManager.current?.getPortals() ?? null} - +
+ {/* IMPORTANT we need 3 layers of divs with NO className because the class is managed by the styled root */} +
+
+
+ + {context && !context.isDestroyed() ? ( + + ) : null} + {portalManager.current?.getPortals() ?? null} + +
+
+
); }; diff --git a/packages/ag-grid-react/src/reactUi/gridComp.tsx b/packages/ag-grid-react/src/reactUi/gridComp.tsx index df152c02bee..b297072d1e6 100644 --- a/packages/ag-grid-react/src/reactUi/gridComp.tsx +++ b/packages/ag-grid-react/src/reactUi/gridComp.tsx @@ -25,7 +25,6 @@ type FocusableContainerComp = Component & FocusableContainer; type HeaderDropZonesComp = Component & { getFocusableContainers?: () => FocusableContainerComp[] }; const GridComp = ({ context }: GridCompProps) => { - const [rtlClass, setRtlClass] = useState(''); const [layoutClass, setLayoutClass] = useState(''); const [cursor, setCursor] = useState(null); const [userSelect, setUserSelect] = useState(null); @@ -60,7 +59,6 @@ const GridComp = ({ context }: GridCompProps) => { const compProxy: IGridComp = { destroyGridUi: () => {}, // do nothing, as framework users destroy grid by removing the comp - setRtlClass, forceFocusOutOfContainer: (up?: boolean) => { if (!up && paginationCompRef.current?.isDisplayed()) { paginationCompRef.current.forceFocusOutOfContainer(up); @@ -201,10 +199,7 @@ const GridComp = ({ context }: GridCompProps) => { }; }, [tabGuardReady, eGridBodyParent, context]); - const rootWrapperClasses = useMemo( - () => classesList('ag-root-wrapper', rtlClass, layoutClass), - [rtlClass, layoutClass] - ); + const rootWrapperClasses = useMemo(() => classesList('ag-root-wrapper', layoutClass), [layoutClass]); const rootWrapperBodyClasses = useMemo( () => classesList('ag-root-wrapper-body', 'ag-focus-managed', layoutClass), [layoutClass] diff --git a/packages/ag-grid-react/src/reactUi/rows/rowComp.tsx b/packages/ag-grid-react/src/reactUi/rows/rowComp.tsx index 1a3ba0c44a8..5ea23a19c69 100644 --- a/packages/ag-grid-react/src/reactUi/rows/rowComp.tsx +++ b/packages/ag-grid-react/src/reactUi/rows/rowComp.tsx @@ -59,10 +59,8 @@ const RowComp = ({ rowCtrl, containerType }: { rowCtrl: RowCtrl; containerType: const eGui = useRef(null); const eFullWidthAnchor = useRef(null); - const ePinnedLeftSection = useRef(null); const ePinnedLeftCells = useRef(null); const eScrollingCells = useRef(null); - const ePinnedRightSection = useRef(null); const ePinnedRightCells = useRef(null); const fullWidthCompRef = useRef(); const fullWidthEmbeddedLeftCompRef = useRef(); @@ -72,7 +70,8 @@ const RowComp = ({ rowCtrl, containerType }: { rowCtrl: RowCtrl; containerType: const fullWidthEmbeddedLeftParamsRef = useRef(); const fullWidthEmbeddedCenterParamsRef = useRef(); const fullWidthEmbeddedRightParamsRef = useRef(); - const [embeddedSectionHasContent, setEmbeddedSectionHasContent] = useState(() => rowCtrl.embeddedSectionHasContent); + const [, setEmbeddedSectionHasContent] = useState(() => rowCtrl.embeddedSectionHasContent); + const [, refreshWidths] = useState(0); const autoHeightSetup = useRef(false); const [autoHeightSetupAttempt, setAutoHeightSetupAttempt] = useState(0); @@ -163,10 +162,9 @@ const RowComp = ({ rowCtrl, containerType }: { rowCtrl: RowCtrl; containerType: } }, getPinnedLeftRowElement: () => ePinnedLeftCells.current ?? undefined, - getPinnedLeftSectionElement: () => ePinnedLeftSection.current ?? undefined, getScrollingRowElement: () => eScrollingCells.current ?? undefined, getPinnedRightRowElement: () => ePinnedRightCells.current ?? undefined, - getPinnedRightSectionElement: () => ePinnedRightSection.current ?? undefined, + refreshPinnedSections: () => refreshWidths((v) => v + 1), showFullWidth: (compDetails) => { embeddedFullWidthCompDetailsRef.current = undefined; setEmbeddedFullWidthCompDetails(undefined); @@ -384,10 +382,7 @@ const RowComp = ({ rowCtrl, containerType }: { rowCtrl: RowCtrl; containerType: }; }, [cellCtrlsMerged]); - const { leftWidth, centerWidth, rightWidth } = useMemo( - () => rowCtrl.getMappedPinnedCellGroupWidths(), - [rowCtrl, showEmbeddedFullWidth, embeddedSectionHasContent] - ); + const { leftWidth, centerWidth, rightWidth } = rowCtrl.getMappedPinnedCellGroupWidths(); const reactFullWidthCellRendererStateless = useMemo(() => { const res = @@ -440,28 +435,37 @@ const RowComp = ({ rowCtrl, containerType }: { rowCtrl: RowCtrl; containerType: const renderCellSection = ( sectionClass: string, - sectionRef: React.Ref | undefined, - wrapperRef: React.Ref, + ref: React.Ref, width: number, children: React.ReactNode, pinned: boolean = false - ) => ( -
0 ? undefined : 'none' } : undefined} - > -
0 ? undefined : 'none' }} - > + ) => { + if ( + // Detach pinned cell containers when there are no pinned columns to + // improve rendering performance + pinned && + width <= 0 && + // Unless we're rendering a full width rows, because the row + // renderer is passed a reference to these even if they are empty + !isFullWidth + ) { + return null; + } + if (pinned) { + return ( +
+
+ {children} +
+
+ ); + } + return ( +
{children}
-
- ); + ); + }; return (
{renderCellSection( 'ag-grid-pinned-left-cells', - ePinnedLeftSection, ePinnedLeftCells, leftWidth, showCellsJsx(leftCellCtrls), @@ -484,14 +487,12 @@ const RowComp = ({ rowCtrl, containerType }: { rowCtrl: RowCtrl; containerType: )} {renderCellSection( 'ag-grid-scrolling-cells', - undefined, eScrollingCells, centerWidth, showCellsJsx(centerCellCtrls) )} {renderCellSection( 'ag-grid-pinned-right-cells', - ePinnedRightSection, ePinnedRightCells, rightWidth, showCellsJsx(rightCellCtrls), @@ -502,7 +503,6 @@ const RowComp = ({ rowCtrl, containerType }: { rowCtrl: RowCtrl; containerType: <> {renderCellSection( 'ag-grid-pinned-left-cells', - ePinnedLeftSection, ePinnedLeftCells, leftWidth, showEmbeddedFrameworkSection('left'), @@ -510,14 +510,12 @@ const RowComp = ({ rowCtrl, containerType }: { rowCtrl: RowCtrl; containerType: )} {renderCellSection( 'ag-grid-scrolling-cells', - undefined, eScrollingCells, centerWidth, showEmbeddedFrameworkSection('center') )} {renderCellSection( 'ag-grid-pinned-right-cells', - ePinnedRightSection, ePinnedRightCells, rightWidth, showEmbeddedFrameworkSection('right'), diff --git a/packages/ag-grid-react/src/reactUi/useIsomorphicLayoutEffect.ts b/packages/ag-grid-react/src/reactUi/useIsomorphicLayoutEffect.ts deleted file mode 100644 index e4b3e8cce7d..00000000000 --- a/packages/ag-grid-react/src/reactUi/useIsomorphicLayoutEffect.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useEffect, useLayoutEffect } from 'react'; - -/** - * Allow `useLayoutEffect` to be used on the server side without warnings - */ -export const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect; diff --git a/packages/ag-stack/src/core/baseDragAndDropService.ts b/packages/ag-stack/src/core/baseDragAndDropService.ts index 6121ac1c9dc..9e9daf464fd 100644 --- a/packages/ag-stack/src/core/baseDragAndDropService.ts +++ b/packages/ag-stack/src/core/baseDragAndDropService.ts @@ -11,6 +11,7 @@ import type { IDragAndDropService, } from '../interfaces/iDragAndDrop'; import type { IPropertiesService } from '../interfaces/iProperties'; +import { _initStyledRoot } from '../theming/styledRoot'; import { _getPageBody, _getRootNode } from '../utils/document'; import { _anchorElementToMouseMoveEvent } from '../utils/event'; import type { AgPromise } from '../utils/promise'; @@ -61,6 +62,7 @@ export abstract class BaseDragAndDropService< private dragImageCompPromise: AgPromise & IDragAndDropImage> | null = null; private dragImageComp: (IComponent & IDragAndDropImage) | null = null; + private disconnect: (() => void) | null = null; private dragImageLastIcon: TDragAndDropIcon | null | undefined = undefined; private dragImageLastLabel: string | null | undefined = undefined; @@ -444,7 +446,8 @@ export abstract class BaseDragAndDropService< this.dragImageComp = null; } if (comp) { - comp.getGui()?.remove(); + this.disconnect?.(); + this.disconnect = null; this.destroyBean(comp); } } @@ -489,7 +492,6 @@ export abstract class BaseDragAndDropService< } this.gos.setInstanceDomData(eGui); - this.beans.environment.applyThemeClasses(eGui); style.top = '20px'; style.left = '20px'; @@ -497,9 +499,9 @@ export abstract class BaseDragAndDropService< const targetEl = _getPageBody(this.beans); if (!targetEl) { this.warnNoBody(); - } else { - targetEl.appendChild(eGui); + return; } + this.disconnect = _initStyledRoot(this.beans.environment, targetEl, eGui); } private updateDragImageComp(): void { diff --git a/packages/ag-stack/src/core/baseEnvironment.ts b/packages/ag-stack/src/core/baseEnvironment.ts index 7040a5e7329..389b5118ea3 100644 --- a/packages/ag-stack/src/core/baseEnvironment.ts +++ b/packages/ag-stack/src/core/baseEnvironment.ts @@ -1,5 +1,5 @@ import type { AgCoreBeanCollection } from '../interfaces/agCoreBeanCollection'; -import type { BaseEvents } from '../interfaces/baseEvents'; +import type { AgStylesChangedEvent, BaseEvents } from '../interfaces/baseEvents'; import type { BaseProperties } from '../interfaces/baseProperties'; import type { IEnvironment } from '../interfaces/iEnvironment'; import type { IPropertiesService } from '../interfaces/iProperties'; @@ -9,6 +9,7 @@ import { _unregisterInstanceUsingThemingAPI, _useParamsCss, } from '../theming/inject'; +import { _initStyledRootFromInnerOfThreeElements } from '../theming/styledRoot'; import type { Theme } from '../theming/theme'; import { ThemeImpl } from '../theming/themeImpl'; import type { ParamType } from '../theming/themeTypeUtils'; @@ -83,53 +84,60 @@ export abstract class BaseEnvironment< this.addManagedPropertyListener('theme', () => this.handleThemeChange()); this.handleThemeChange(); - this.getSizeEl(LIST_ITEM_HEIGHT); - this.initVariables(); - - this.addDestroyFunc(() => _unregisterInstanceUsingThemingAPI(this)); - this.mutationObserver = new MutationObserver(() => { this.fireStylesChangedEvent('theme'); }); this.addDestroyFunc(() => this.mutationObserver.disconnect()); + + this.addDestroyFunc(_initStyledRootFromInnerOfThreeElements(this, eRootDiv)); + this.getSizeEl(LIST_ITEM_HEIGHT); + this.initVariables(); + + this.addDestroyFunc(() => _unregisterInstanceUsingThemingAPI(this)); } - public applyThemeClasses(el: HTMLElement, extraClasses: string[] = []): void { + public getStyledRootClasses(): [inheritClass: string, applyClass: string, directionClass: string] { const { theme } = this; - const themeClass = theme ? theme._getCssClass() : this.applyLegacyThemeClasses(); - - for (const className of Array.from(el.classList)) { - if (className.startsWith('ag-theme-')) { - el.classList.remove(className); - } - } - if (themeClass) { - const oldClass = el.className; - el.className = `${oldClass}${oldClass ? ' ' : ''}${themeClass}${extraClasses?.length ? ' ' + extraClasses.join(' ') : ''}`; - } + const [inheritClass, applyClass] = theme ? theme._getCssClasses() : ['', this.getLegacyThemeClasses()]; + const directionClass = this.gos.get('enableRtl') ? 'ag-rtl' : 'ag-ltr'; + return [inheritClass, applyClass, directionClass]; } - private applyLegacyThemeClasses(): string { - let themeClass = ''; + private getLegacyThemeClasses(): string { + const themeClasses = new Set(); + // rebuild the observer set every time we call this function, to handle + // edge cases where the grid is initialised outside the DOM or moved this.mutationObserver.disconnect(); - let node: HTMLElement | null = this.eRootDiv; + let node = this.eRootDiv.parentElement; while (node) { - let isThemeEl = false; - for (const className of Array.from(node.classList)) { - if (className.startsWith('ag-theme-')) { - isThemeEl = true; - themeClass = themeClass ? `${themeClass} ${className}` : className; + if (!node.classList.contains('ag-styled-root')) { + let isThemeEl = false; + for (const cls of node.classList) { + if (cls.startsWith('ag-theme-')) { + isThemeEl = true; + themeClasses.add(cls); + } + } + if (isThemeEl) { + this.mutationObserver.observe(node, { + attributes: true, + attributeFilter: ['class'], + }); } - } - if (isThemeEl) { - this.mutationObserver.observe(node, { - attributes: true, - attributeFilter: ['class'], - }); } node = node.parentElement; } - return themeClass; + return [...themeClasses].join(' '); + } + + public onThemeChanged(handler: () => void): () => void { + const listener = (e: AgStylesChangedEvent) => { + if (e.themeChanged) { + handler(); + } + }; + this.eventSvc.addListener('stylesChanged', listener); + return () => this.eventSvc.removeListener('stylesChanged', listener); } public addGlobalCSS(css: string, debugId: string): void { @@ -263,7 +271,7 @@ export abstract class BaseEnvironment< } private handleNewTheme(newTheme: ThemeImpl | undefined): void { - const { gos, eRootDiv, globalCSS } = this; + const { gos, globalCSS } = this; const additionalCss = this.getAdditionalCss(); if (newTheme) { _injectCoreAndModuleCSS(this.eStyleContainer, this.cssLayer, this.styleNonce, additionalCss); @@ -290,7 +298,6 @@ export abstract class BaseEnvironment< this.styleNonce ); - this.applyThemeClasses(eRootDiv); this.fireStylesChangedEvent('theme'); } diff --git a/packages/ag-stack/src/interfaces/iEnvironment.ts b/packages/ag-stack/src/interfaces/iEnvironment.ts index fa1a3f5f4f5..ad424308542 100644 --- a/packages/ag-stack/src/interfaces/iEnvironment.ts +++ b/packages/ag-stack/src/interfaces/iEnvironment.ts @@ -3,7 +3,11 @@ export interface IEnvironment { addGlobalCSS(css: string, debugId: string): void; - applyThemeClasses(el: HTMLElement): void; - getDefaultListItemHeight(): number; + + /** Returns `[inheritClass, applyClass, directionClass]` for the three styled-root levels. */ + getStyledRootClasses(): [inheritClass: string, applyClass: string, directionClass: string]; + + /** Subscribes to theme-change events. Returns an unsubscribe fn. */ + onThemeChanged(handler: () => void): () => void; } diff --git a/packages/ag-stack/src/main-internal.ts b/packages/ag-stack/src/main-internal.ts index 2e94153f452..1c6d3587f2e 100644 --- a/packages/ag-stack/src/main-internal.ts +++ b/packages/ag-stack/src/main-internal.ts @@ -75,6 +75,7 @@ export { AutoScrollService } from './rendering/autoScrollService'; export { CssClassManager } from './rendering/cssClassManager'; export { defaultFontFamily, defaultLightColorSchemeParams, sharedDefaults } from './theming/shared/shared-css'; export type { SharedThemeParams } from './theming/shared/shared-css'; +export { _createStyledRootElements, _initDetachedStyledRoot, _initStyledRoot } from './theming/styledRoot'; export { _asThemeImpl, createSharedTheme, ThemeImpl } from './theming/themeImpl'; export type { ThemeLogger } from './theming/themeLogger'; export { diff --git a/packages/ag-stack/src/popup/basePopupService.ts b/packages/ag-stack/src/popup/basePopupService.ts index 4bb57fd869d..e1e971410c4 100644 --- a/packages/ag-stack/src/popup/basePopupService.ts +++ b/packages/ag-stack/src/popup/basePopupService.ts @@ -2,7 +2,7 @@ import { Direction } from '../constants/direction'; import { KeyCode } from '../constants/keyCode'; import { AgBeanStub } from '../core/agBeanStub'; import type { AgCoreBeanCollection } from '../interfaces/agCoreBeanCollection'; -import type { AgStylesChangedEvent, BaseEvents } from '../interfaces/baseEvents'; +import type { BaseEvents } from '../interfaces/baseEvents'; import type { BaseProperties } from '../interfaces/baseProperties'; import type { AddPopupParams, @@ -15,6 +15,7 @@ import type { } from '../interfaces/iPopup'; import type { IPopupService } from '../interfaces/iPopupService'; import type { IPropertiesService } from '../interfaces/iProperties'; +import { _initStyledRoot } from '../theming/styledRoot'; import { _setAriaLabel, _setAriaOwns, _setAriaRole } from '../utils/aria'; import { _getActiveDomElement, _getDocument } from '../utils/document'; import { @@ -31,7 +32,6 @@ import { computeAlignedPosition, toRelativeRect } from './popupPositionUtils'; interface AgPopup { element: HTMLElement; - wrapper: HTMLElement; hideFunc: (params?: PopupEventParams) => void; isAnchored: boolean; instanceId: number; @@ -66,10 +66,6 @@ export abstract class BasePopupService< protected popupList: AgPopup[] = []; - public postConstruct(): void { - this.addManagedEventListeners({ stylesChanged: this.handleThemeChange.bind(this) }); - } - public getPopupParent(): HTMLElement { const ePopupParent = this.gos.get('popupParent'); @@ -409,14 +405,36 @@ export abstract class BasePopupService< this.initialisePopupPosition(eChild); - const wrapperEl = this.createPopupWrapper(eChild, !!alwaysOnTop, ariaLabel, ariaOwns); - const removeListeners = this.addEventListenersToPopup({ ...params, wrapperEl }); + eChild.classList.add('ag-popup-child'); + + if (!eChild.hasAttribute('role')) { + _setAriaRole(eChild, 'dialog'); + } + + if (ariaLabel) { + _setAriaLabel(eChild, ariaLabel); + } else if (ariaOwns) { + eChild.id ||= `popup-component-${instanceIdSeq}`; + _setAriaOwns(ariaOwns, eChild.id); + } + + const wrapperEl = _createAgElement({ tag: 'div', cls: 'ag-popup' }); + wrapperEl.appendChild(eChild); + const disconnect = _initStyledRoot(this.beans.environment, this.getPopupParent(), wrapperEl); + + if (alwaysOnTop) { + this.setAlwaysOnTop(eChild, true); + } else { + this.bringPopupToFront(eChild); + } + + const removeListeners = this.addEventListenersToPopup({ ...params, wrapperEl, disconnect }); if (positionCallback) { positionCallback(); } - this.addPopupToPopupList(eChild, wrapperEl, removeListeners, anchorToElement); + this.addPopupToPopupList(eChild, removeListeners, anchorToElement); return { hideFunc: removeListeners, @@ -435,50 +453,10 @@ export abstract class BasePopupService< } } - private createPopupWrapper( - element: HTMLElement, - alwaysOnTop: boolean, - ariaLabel?: string, - ariaOwns?: HTMLElement - ): HTMLElement { - const ePopupParent = this.getPopupParent(); - - // add env CSS class to child, in case user provided a popup parent, which means - // theme class may be missing - const { environment, gos } = this.beans; - const eWrapper = _createAgElement({ tag: 'div' }); - environment.applyThemeClasses(eWrapper); - - eWrapper.classList.add('ag-popup'); - element.classList.add(gos.get('enableRtl') ? 'ag-rtl' : 'ag-ltr', 'ag-popup-child'); - - if (!element.hasAttribute('role')) { - _setAriaRole(element, 'dialog'); - } - - if (ariaLabel) { - _setAriaLabel(element, ariaLabel); - } else if (ariaOwns) { - element.id ||= `popup-component-${instanceIdSeq}`; - _setAriaOwns(ariaOwns, element.id); - } - - eWrapper.appendChild(element); - ePopupParent.appendChild(eWrapper); - - if (alwaysOnTop) { - this.setAlwaysOnTop(element, true); - } else { - this.bringPopupToFront(element); - } - - return eWrapper; - } - protected abstract isStopPropagation(event: Event): boolean; private addEventListenersToPopup( - params: AddPopupParams & { wrapperEl: HTMLElement } + params: AddPopupParams & { wrapperEl: HTMLElement; disconnect: () => void } ): (popupParams?: PopupEventParams) => void { const beans = this.beans; const eDocument = _getDocument(beans); @@ -518,7 +496,7 @@ export abstract class BasePopupService< popupHidden = true; - wrapperEl.remove(); + params.disconnect(); eDocument.removeEventListener('keydown', hidePopupOnKeyboardEvent); eDocument.removeEventListener('mousedown', hidePopupOnMouseEvent); @@ -558,13 +536,11 @@ export abstract class BasePopupService< private addPopupToPopupList( element: HTMLElement, - wrapperEl: HTMLElement, removeListeners: (popupParams?: PopupEventParams) => void, anchorToElement?: HTMLElement ): void { this.popupList.push({ element: element, - wrapper: wrapperEl, hideFunc: removeListeners, instanceId: instanceIdSeq, isAnchored: !!anchorToElement, @@ -829,13 +805,4 @@ export abstract class BasePopupService< currentEl![0].scrollTop = currentEl![1]; } } - - private handleThemeChange(e: AgStylesChangedEvent) { - if (e.themeChanged) { - const environment = this.beans.environment; - for (const popup of this.popupList) { - environment.applyThemeClasses(popup.wrapper); - } - } - } } diff --git a/packages/ag-stack/src/theming/shared/css/_popup.css b/packages/ag-stack/src/theming/shared/css/_popup.css index cebf08dfb0b..55dc31f9730 100644 --- a/packages/ag-stack/src/theming/shared/css/_popup.css +++ b/packages/ag-stack/src/theming/shared/css/_popup.css @@ -1,3 +1,7 @@ +.ag-popup { + background-color: var(--ag-wrapper-background-color); +} + .ag-popup-child { /* grid-managed popups must sit above overlays and row content. */ z-index: 5; diff --git a/packages/ag-stack/src/theming/shared/css/_reset.css b/packages/ag-stack/src/theming/shared/css/_reset.css index 219b78e7e47..5ed6858d9fe 100644 --- a/packages/ag-stack/src/theming/shared/css/_reset.css +++ b/packages/ag-stack/src/theming/shared/css/_reset.css @@ -1,6 +1,3 @@ -/* NOTE: this list of root selectors is present in _root.css too, if you update - one then don't forget the other */ - /* stylelint-disable-next-line ag/no-low-performance-key-selector */ :where([class^='ag-']), :where([class^='ag-'])::after, diff --git a/packages/ag-stack/src/theming/shared/css/_root.css b/packages/ag-stack/src/theming/shared/css/_root.css index edc49de9f5c..e9e22c23d77 100644 --- a/packages/ag-stack/src/theming/shared/css/_root.css +++ b/packages/ag-stack/src/theming/shared/css/_root.css @@ -1,10 +1,5 @@ -/* NOTE: this list of root selectors is present in _reset.css too, if you update - one then don't forget the other */ -.ag-root-wrapper, -.ag-popup, -.ag-dnd-ghost, -.ag-external, -.ag-chart { +.ag-styled-root { + display: contents; line-height: normal; cursor: default; white-space: normal; @@ -14,7 +9,6 @@ font-weight: var(--ag-font-weight); color-scheme: var(--ag-browser-color-scheme); color: var(--ag-text-color); - background-color: var(--ag-wrapper-background-color); --ag-indentation-level: 0; } diff --git a/packages/ag-stack/src/theming/styledRoot.ts b/packages/ag-stack/src/theming/styledRoot.ts new file mode 100644 index 00000000000..6faf4df0665 --- /dev/null +++ b/packages/ag-stack/src/theming/styledRoot.ts @@ -0,0 +1,44 @@ +import type { IEnvironment } from '../interfaces/iEnvironment'; +import { _createAgElement } from '../utils/dom'; + +/** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */ +export function _initDetachedStyledRoot( + env: IEnvironment, + child: HTMLElement +): [element: HTMLElement, destroy: () => void] { + const [outer, inner] = _createStyledRootElements(); + inner.appendChild(child); + const destroy = _initStyledRootFromInnerOfThreeElements(env, inner); + return [outer, destroy]; +} + +/** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */ +export function _initStyledRoot(env: IEnvironment, parent: HTMLElement | ShadowRoot, child: HTMLElement): () => void { + const [element, destroy] = _initDetachedStyledRoot(env, child); + parent.appendChild(element); + return () => { + destroy(); + element.remove(); + }; +} + +/** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */ +export function _createStyledRootElements(): [outer: HTMLElement, inner: HTMLElement] { + const el = { tag: 'div', cls: 'ag-styled-root' } as const; + const outer = _createAgElement({ ...el, children: [{ ...el, children: [el] }] }); + return [outer, outer.firstElementChild!.firstElementChild as HTMLElement]; +} + +/** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */ +export function _initStyledRootFromInnerOfThreeElements(env: IEnvironment, inner: HTMLElement): () => void { + const middle = inner.parentElement!; + const outer = middle.parentElement!; + const applyClasses = () => { + const [inheritClass, applyClass, directionClass] = env.getStyledRootClasses(); + outer.className = ['ag-styled-root', inheritClass].join(' '); + middle.className = ['ag-styled-root', applyClass].join(' '); + inner.className = ['ag-styled-root', directionClass].join(' '); + }; + applyClasses(); + return env.onThemeChanged(applyClasses); +} diff --git a/packages/ag-stack/src/theming/themeImpl.ts b/packages/ag-stack/src/theming/themeImpl.ts index d4d37d6b017..06c90d78613 100644 --- a/packages/ag-stack/src/theming/themeImpl.ts +++ b/packages/ag-stack/src/theming/themeImpl.ts @@ -96,24 +96,30 @@ export class ThemeImpl { } } - private _cssClassCache?: string; + private _cssClassCache?: [string, string]; - _getCssClass(this: ThemeImpl): string { + _getCssClasses(this: ThemeImpl): [inheritClasses: string, applyClasses: string] { if (FORCE_LEGACY_THEMES) { - return 'ag-theme-quartz'; + return ['', 'ag-theme-quartz']; } - return (this._cssClassCache ??= deduplicatePartsByFeature(this.parts) - .map((part) => part.use(undefined, undefined, undefined)) - .filter(Boolean) - .concat(this._getParamsClassName()) - .join(' ')); + return (this._cssClassCache ??= [ + this._getParamsClassName(true), + deduplicatePartsByFeature(this.parts) + .map((part) => part.use(undefined, undefined, undefined)) + .filter(Boolean) + .concat(this._getParamsClassName()) + .join(' '), + ]); } - private _paramsClassName?: string; + private _classNamesId?: number; + private _getClassNamesId(): number { + return (this._classNamesId ??= ++getInjectionState().paramsId); + } - _getParamsClassName(): string { - return (this._paramsClassName ??= `ag-theme-params-${++getInjectionState().paramsId}`); + _getParamsClassName(inherit = false): string { + return `ag-theme-${inherit ? 'inherit' : 'params'}-${this._getClassNamesId()}`; } private _paramsCache?: ModalParamValues; @@ -192,7 +198,7 @@ export class ThemeImpl { const params = modeParams[mode]; if (mode !== defaultModeName) { const escapedMode = typeof CSS === 'object' ? CSS.escape(mode) : mode; // check for CSS global in case we're running in tests - const wrapPrefix = `:where([data-ag-theme-mode="${escapedMode}"]) & {\n`; + const wrapPrefix = `:where(html[data-ag-theme-mode="${escapedMode}"],body[data-ag-theme-mode="${escapedMode}"],.ag-theme-mode[data-ag-theme-mode="${escapedMode}"]) & {\n`; variablesCss += wrapPrefix; inheritanceCss += wrapPrefix; } @@ -215,12 +221,8 @@ export class ThemeImpl { inheritanceCss += '}\n'; } } - const selectorPlaceholder = `:where(.${this._getParamsClassName()})`; - let css = `${selectorPlaceholder} {\n${variablesCss}}\n`; - // Create --ag-inherited-foo variable values on the parent element, unless - // the parent is itself a root (which can happen if popupParent is - // ag-root-wrapper) - css += `:has(> ${selectorPlaceholder}):not(${selectorPlaceholder}) {\n${inheritanceCss}}\n`; + let css = `:where(.${this._getParamsClassName()}) {\n${variablesCss}}\n`; + css += `:where(.${this._getParamsClassName(true)}) {\n${inheritanceCss}}\n`; this._paramsCssCache = css; } return this._paramsCssCache;