diff --git a/src/components/common/controllers/resize-observer.ts b/src/components/common/controllers/resize-observer.ts new file mode 100644 index 000000000..1a88ef906 --- /dev/null +++ b/src/components/common/controllers/resize-observer.ts @@ -0,0 +1,91 @@ +import { + type ReactiveController, + type ReactiveControllerHost, + isServer, +} from 'lit'; + +type ResizeControllerCallback = ( + ...args: Parameters +) => unknown; + +/** Configuration for initializing a resize controller. */ +export interface ResizeControllerConfig { + /** The callback function to run when a resize mutation is triggered. */ + callback: ResizeControllerCallback; + /** Configuration options passed to the underlying ResizeObserver. */ + options?: ResizeObserverOptions; + /** + * The initial target element to observe for resize mutations. + * + * If not provided, the host element will be set as initial target. + * Pass in `null` to skip setting an initial target. + */ + target?: Element | null; +} + +class ResizeController implements ReactiveController { + private _host: ReactiveControllerHost & Element; + private _targets = new Set(); + private _config: ResizeControllerConfig; + private _observer!: ResizeObserver; + + constructor( + host: ReactiveControllerHost & Element, + config: ResizeControllerConfig + ) { + this._host = host; + this._config = config; + + if (this._config.target !== null) { + this._targets.add(this._config.target ?? host); + } + + if (isServer) { + return; + } + + this._observer = new ResizeObserver((entries) => + this._config.callback.call(this._host, entries, this._observer) + ); + + host.addController(this); + } + + /** Starts observing the `targe` element. */ + public observe(target: Element): void { + this._targets.add(target); + this._observer.observe(target, this._config.options); + this._host.requestUpdate(); + } + + /** Stops observing the `target` element. */ + public unobserve(target: Element): void { + this._targets.delete(target); + this._observer.unobserve(target); + } + + public hostConnected(): void { + for (const target of this._targets) { + this.observe(target); + } + } + + public hostDisconnected(): void { + this.disconnect(); + } + + protected disconnect(): void { + this._observer.disconnect(); + } +} + +/** + * Creates a new resize controller bound to the given `host` + * with {@link ResizeControllerConfig | `config`}. + */ +export function createResizeController( + host: ReactiveControllerHost & Element, + config: ResizeControllerConfig +) { + return new ResizeController(host, config); +} diff --git a/src/components/common/definitions/defineAllComponents.ts b/src/components/common/definitions/defineAllComponents.ts index 097ce63f5..996eed47f 100644 --- a/src/components/common/definitions/defineAllComponents.ts +++ b/src/components/common/definitions/defineAllComponents.ts @@ -56,7 +56,6 @@ import IgcSliderComponent from '../../slider/slider.js'; import IgcSnackbarComponent from '../../snackbar/snackbar.js'; import IgcStepComponent from '../../stepper/step.js'; import IgcStepperComponent from '../../stepper/stepper.js'; -import IgcTabPanelComponent from '../../tabs/tab-panel.js'; import IgcTabComponent from '../../tabs/tab.js'; import IgcTabsComponent from '../../tabs/tabs.js'; import IgcTextareaComponent from '../../textarea/textarea.js'; @@ -123,7 +122,6 @@ const allComponents: IgniteComponent[] = [ IgcRangeSliderComponent, IgcTabsComponent, IgcTabComponent, - IgcTabPanelComponent, IgcCircularProgressComponent, IgcLinearProgressComponent, IgcCircularGradientComponent, diff --git a/src/components/common/util.ts b/src/components/common/util.ts index e31ff8ea1..2543ed714 100644 --- a/src/components/common/util.ts +++ b/src/components/common/util.ts @@ -27,34 +27,6 @@ export function sameObject(a: object, b: object) { return JSON.stringify(a) === JSON.stringify(b); } -/** - * - * Returns an element's offset relative to its parent. Similar to element.offsetTop and element.offsetLeft, except the - * parent doesn't have to be positioned relative or absolute. - * - * Work around for the following issues in Chromium based browsers: - * - * https://bugs.chromium.org/p/chromium/issues/detail?id=1330819 - * https://bugs.chromium.org/p/chromium/issues/detail?id=1334556 - * - */ -export function getOffset(element: HTMLElement, parent: HTMLElement) { - const { top, left, bottom, right } = element.getBoundingClientRect(); - const { - top: pTop, - left: pLeft, - bottom: pBottom, - right: pRight, - } = parent.getBoundingClientRect(); - - return { - top: Math.round(top - pTop), - left: Math.round(left - pLeft), - right: Math.round(right - pRight), - bottom: Math.round(bottom - pBottom), - }; -} - export function createCounter() { let i = 0; return () => { @@ -288,3 +260,23 @@ export function asArray(value?: T | T[]): T[] { if (!isDefined(value)) return []; return Array.isArray(value) ? value : [value]; } + +export function scrollIntoView( + element?: HTMLElement, + config?: ScrollIntoViewOptions +): void { + if (!element) { + return; + } + + element.scrollIntoView( + Object.assign( + { + behavior: 'auto', + block: 'nearest', + inline: 'nearest', + }, + config + ) + ); +} diff --git a/src/components/tabs/tab-panel.ts b/src/components/tabs/tab-panel.ts deleted file mode 100644 index d799c63cd..000000000 --- a/src/components/tabs/tab-panel.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { LitElement, html } from 'lit'; - -import { registerComponent } from '../common/definitions/register.js'; -import { createCounter } from '../common/util.js'; -import { styles } from './themes/tab-panel.base.css.js'; - -/** - * Represents the content of a tab - * - * @element igc-tab-panel - * - * @slot - Renders the content. - */ -export default class IgcTabPanelComponent extends LitElement { - public static readonly tagName = 'igc-tab-panel'; - public static override styles = styles; - - /* blazorSuppress */ - public static register() { - registerComponent(IgcTabPanelComponent); - } - - private static readonly increment = createCounter(); - - public override connectedCallback() { - super.connectedCallback(); - this.setAttribute('role', 'tabpanel'); - this.tabIndex = this.hasAttribute('tabindex') ? this.tabIndex : 0; - this.slot = this.slot.length > 0 ? this.slot : 'panel'; - this.id = - this.getAttribute('id') || - `igc-tab-panel-${IgcTabPanelComponent.increment()}`; - } - - protected override render() { - return html``; - } -} - -declare global { - interface HTMLElementTagNameMap { - 'igc-tab-panel': IgcTabPanelComponent; - } -} diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index 61a65d222..bee91c6e7 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -1,5 +1,5 @@ import { LitElement, html } from 'lit'; -import { property, query } from 'lit/decorators.js'; +import { property } from 'lit/decorators.js'; import { themes } from '../../theming/theming-decorator.js'; import { registerComponent } from '../common/definitions/register.js'; @@ -13,13 +13,16 @@ import { styles } from './themes/tab.base.css.js'; * * @element igc-tab * - * @slot prefix - Renders before the tab header content. - * @slot - Renders the tab header content. - * @slot suffix - Renders after the tab header content. + * @slot - Renders the tab's content. + * @slot label - Renders the tab header's label. + * @slot prefix - Renders the tab header's prefix. + * @slot suffix - Renders the tab header's suffix. * - * @csspart content - The content wrapper. - * @csspart prefix - The prefix wrapper. - * @csspart suffix - The suffix wrapper. + * @csspart tab-header - The header of a single tab. + * @csspart prefix - Tab header's label prefix. + * @csspart content - Tab header's label slot container. + * @csspart suffix - Tab header's label suffix. + * @csspart tab-body - Holds the body content of a single tab, only the body of the selected tab is visible. */ @themes(all) @@ -32,17 +35,14 @@ export default class IgcTabComponent extends LitElement { registerComponent(IgcTabComponent); } - private static readonly increment = createCounter(); - - @query('[part="base"]', true) - private tab!: HTMLElement; + private static increment = createCounter(); /** - * The id of the tab panel which will be controlled by the tab. + * The tab item label. * @attr */ @property() - public panel = ''; + public label = ''; /** * Determines whether the tab is selected. @@ -60,34 +60,39 @@ export default class IgcTabComponent extends LitElement { public override connectedCallback(): void { super.connectedCallback(); - this.id = - this.getAttribute('id') || `igc-tab-${IgcTabComponent.increment()}`; - } - - /** Sets focus to the tab. */ - public override focus(options?: FocusOptions) { - this.tab.focus(options); - } - - /** Removes focus from the tab. */ - public override blur() { - this.tab.blur(); + this.id = this.id || `igc-tab-${IgcTabComponent.increment()}`; } protected override render() { + const headerId = `${this.id}-header`; + const contentId = `${this.id}-content`; + return html`