Skip to content

Commit b227ef4

Browse files
MonikaKirkovarkaraivanovdesig9steinsimeonoff
authored
Refactor Tab & Tabs components (#1352)
--------- Co-authored-by: Radoslav Karaivanov <[email protected]> Co-authored-by: Marin Popov <[email protected]> Co-authored-by: Simeon Simeonoff <[email protected]> Co-authored-by: desig9stein <[email protected]>
1 parent 34e7746 commit b227ef4

27 files changed

+1855
-1053
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
type ReactiveController,
3+
type ReactiveControllerHost,
4+
isServer,
5+
} from 'lit';
6+
7+
type ResizeControllerCallback = (
8+
...args: Parameters<ResizeObserverCallback>
9+
) => unknown;
10+
11+
/** Configuration for initializing a resize controller. */
12+
export interface ResizeControllerConfig {
13+
/** The callback function to run when a resize mutation is triggered. */
14+
callback: ResizeControllerCallback;
15+
/** Configuration options passed to the underlying ResizeObserver. */
16+
options?: ResizeObserverOptions;
17+
/**
18+
* The initial target element to observe for resize mutations.
19+
*
20+
* If not provided, the host element will be set as initial target.
21+
* Pass in `null` to skip setting an initial target.
22+
*/
23+
target?: Element | null;
24+
}
25+
26+
class ResizeController implements ReactiveController {
27+
private readonly _host: ReactiveControllerHost & Element;
28+
private readonly _targets = new Set<Element>();
29+
private readonly _observer!: ResizeObserver;
30+
private readonly _config: ResizeControllerConfig;
31+
32+
constructor(
33+
host: ReactiveControllerHost & Element,
34+
config: ResizeControllerConfig
35+
) {
36+
this._host = host;
37+
this._config = config;
38+
39+
if (this._config.target !== null) {
40+
this._targets.add(this._config.target ?? host);
41+
}
42+
43+
/* c8 ignore next 3 */
44+
if (isServer) {
45+
return;
46+
}
47+
48+
this._observer = new ResizeObserver((entries) =>
49+
this._config.callback.call(this._host, entries, this._observer)
50+
);
51+
52+
host.addController(this);
53+
}
54+
55+
/** Starts observing the `targe` element. */
56+
public observe(target: Element): void {
57+
this._targets.add(target);
58+
this._observer.observe(target, this._config.options);
59+
this._host.requestUpdate();
60+
}
61+
62+
/** Stops observing the `target` element. */
63+
public unobserve(target: Element): void {
64+
this._targets.delete(target);
65+
this._observer.unobserve(target);
66+
}
67+
68+
/** @internal */
69+
public hostConnected(): void {
70+
for (const target of this._targets) {
71+
this.observe(target);
72+
}
73+
}
74+
75+
/** @internal */
76+
public hostDisconnected(): void {
77+
this._observer.disconnect();
78+
}
79+
}
80+
81+
/**
82+
* Creates a new resize controller bound to the given `host`
83+
* with {@link ResizeControllerConfig | `config`}.
84+
*/
85+
export function createResizeController(
86+
host: ReactiveControllerHost & Element,
87+
config: ResizeControllerConfig
88+
): ResizeController {
89+
return new ResizeController(host, config);
90+
}

src/components/common/definitions/defineAllComponents.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import IgcSliderComponent from '../../slider/slider.js';
5656
import IgcSnackbarComponent from '../../snackbar/snackbar.js';
5757
import IgcStepComponent from '../../stepper/step.js';
5858
import IgcStepperComponent from '../../stepper/stepper.js';
59-
import IgcTabPanelComponent from '../../tabs/tab-panel.js';
6059
import IgcTabComponent from '../../tabs/tab.js';
6160
import IgcTabsComponent from '../../tabs/tabs.js';
6261
import IgcTextareaComponent from '../../textarea/textarea.js';
@@ -126,7 +125,6 @@ const allComponents: IgniteComponent[] = [
126125
IgcRangeSliderComponent,
127126
IgcTabsComponent,
128127
IgcTabComponent,
129-
IgcTabPanelComponent,
130128
IgcCircularProgressComponent,
131129
IgcLinearProgressComponent,
132130
IgcCircularGradientComponent,

src/components/common/util.ts

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,34 +33,6 @@ export function numberInRangeInclusive(
3333
return value >= min && value <= max;
3434
}
3535

36-
/**
37-
*
38-
* Returns an element's offset relative to its parent. Similar to element.offsetTop and element.offsetLeft, except the
39-
* parent doesn't have to be positioned relative or absolute.
40-
*
41-
* Work around for the following issues in Chromium based browsers:
42-
*
43-
* https://bugs.chromium.org/p/chromium/issues/detail?id=1330819
44-
* https://bugs.chromium.org/p/chromium/issues/detail?id=1334556
45-
*
46-
*/
47-
export function getOffset(element: HTMLElement, parent: HTMLElement) {
48-
const { top, left, bottom, right } = element.getBoundingClientRect();
49-
const {
50-
top: pTop,
51-
left: pLeft,
52-
bottom: pBottom,
53-
right: pRight,
54-
} = parent.getBoundingClientRect();
55-
56-
return {
57-
top: Math.round(top - pTop),
58-
left: Math.round(left - pLeft),
59-
right: Math.round(right - pRight),
60-
bottom: Math.round(bottom - pBottom),
61-
};
62-
}
63-
6436
export function createCounter() {
6537
let i = 0;
6638
return () => {
@@ -361,6 +333,26 @@ export function roundByDPR(value: number): number {
361333
return Math.round(value * dpr) / dpr;
362334
}
363335

336+
export function scrollIntoView(
337+
element?: HTMLElement,
338+
config?: ScrollIntoViewOptions
339+
): void {
340+
if (!element) {
341+
return;
342+
}
343+
344+
element.scrollIntoView(
345+
Object.assign(
346+
{
347+
behavior: 'auto',
348+
block: 'nearest',
349+
inline: 'nearest',
350+
},
351+
config
352+
)
353+
);
354+
}
355+
364356
export function isRegExp(value: unknown): value is RegExp {
365357
return value != null && value.constructor === RegExp;
366358
}

src/components/tabs/tab-dom.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import type { Ref } from 'lit/directives/ref.js';
2+
import { isLTR } from '../common/util.js';
3+
import type IgcTabComponent from './tab.js';
4+
import type IgcTabsComponent from './tabs.js';
5+
6+
class TabsHelpers {
7+
private static readonly SCROLL_AMOUNT = 180;
8+
private readonly _host: IgcTabsComponent;
9+
private readonly _container: Ref<HTMLElement>;
10+
private readonly _indicator: Ref<HTMLElement>;
11+
12+
private _styleProperties = {
13+
'--_tabs-count': '',
14+
'--_ig-tabs-width': '',
15+
};
16+
17+
private _hasScrollButtons = false;
18+
private _scrollButtonsDisabled = { start: true, end: false };
19+
20+
private _isLeftToRight = false;
21+
22+
/**
23+
* Returns the DOM container holding the tabs headers.
24+
*/
25+
public get container(): HTMLElement {
26+
return this._container.value!;
27+
}
28+
29+
/**
30+
* Returns the selected indicator DOM element.
31+
*/
32+
public get indicator(): HTMLElement {
33+
return this._indicator.value!;
34+
}
35+
36+
/**
37+
* Returns the internal CSS variables used for the layout of the tabs component.
38+
*/
39+
public get styleProperties() {
40+
return this._styleProperties;
41+
}
42+
43+
/**
44+
* Whether the scroll buttons of the tabs header strip should be shown.
45+
*/
46+
public get hasScrollButtons(): boolean {
47+
return this._hasScrollButtons;
48+
}
49+
50+
/**
51+
* Returns the disabled state of the tabs header strip scroll buttons.
52+
*/
53+
public get scrollButtonsDisabled() {
54+
return this._scrollButtonsDisabled;
55+
}
56+
57+
public get isLeftToRightChanged(): boolean {
58+
const isLeftToRight = isLTR(this._host);
59+
60+
if (this._isLeftToRight !== isLeftToRight) {
61+
this._isLeftToRight = isLeftToRight;
62+
return true;
63+
}
64+
65+
return false;
66+
}
67+
68+
constructor(
69+
host: IgcTabsComponent,
70+
container: Ref<HTMLElement>,
71+
indicator: Ref<HTMLElement>
72+
) {
73+
this._host = host;
74+
this._container = container;
75+
this._indicator = indicator;
76+
}
77+
78+
/**
79+
* Sets the internal CSS variables used for the layout of the tabs component.
80+
* Triggers an update cycle (rerender) of the `igc-tabs` component.
81+
*/
82+
public setStyleProperties(): void {
83+
this._styleProperties = {
84+
'--_tabs-count': this._host.tabs.length.toString(),
85+
'--_ig-tabs-width': `${this.container.getBoundingClientRect().width}px`,
86+
};
87+
this._host.requestUpdate();
88+
}
89+
90+
/**
91+
* Sets the type of the `scroll-snap-align` CSS property for the tabs header strip.
92+
*/
93+
public setScrollSnap(type?: 'start' | 'end'): void {
94+
this.container.style.setProperty('--_ig-tab-snap', type || 'unset');
95+
}
96+
97+
/**
98+
* Scrolls the tabs header strip in the given direction with `scroll-snap-align` set.
99+
*/
100+
public scrollTabs(direction: 'start' | 'end'): void {
101+
const factor = isLTR(this._host) ? 1 : -1;
102+
const amount =
103+
direction === 'start'
104+
? -TabsHelpers.SCROLL_AMOUNT
105+
: TabsHelpers.SCROLL_AMOUNT;
106+
107+
this.setScrollSnap(direction);
108+
this.container.scrollBy({ left: factor * amount, behavior: 'smooth' });
109+
}
110+
111+
/**
112+
* Updates the state of the tabs header strip scroll buttons - visibility and active state.
113+
* Triggers an update cycle (rerender) of the `igc-tabs` component.
114+
*/
115+
public setScrollButtonState(): void {
116+
const { scrollLeft, scrollWidth, clientWidth } = this.container;
117+
118+
this._hasScrollButtons = scrollWidth > clientWidth;
119+
this._scrollButtonsDisabled = {
120+
start: scrollLeft === 0,
121+
end: Math.abs(Math.abs(scrollLeft) + clientWidth - scrollWidth) <= 1,
122+
};
123+
124+
this._host.requestUpdate();
125+
}
126+
127+
/**
128+
* Updates the indicator DOM element styles based on the current "active" tab.
129+
*/
130+
public async setIndicator(active?: IgcTabComponent): Promise<void> {
131+
const styles = {
132+
visibility: active ? 'visible' : 'hidden',
133+
} satisfies Partial<CSSStyleDeclaration>;
134+
135+
await this._host.updateComplete;
136+
137+
if (active) {
138+
const tabHeader = getTabHeader(active);
139+
const { width } = tabHeader.getBoundingClientRect();
140+
141+
const offset = this._isLeftToRight
142+
? tabHeader.offsetLeft - this.container.offsetLeft
143+
: width +
144+
tabHeader.offsetLeft -
145+
this.container.getBoundingClientRect().width;
146+
147+
Object.assign(styles, {
148+
width: `${width}px`,
149+
transform: `translateX(${offset}px)`,
150+
});
151+
}
152+
153+
Object.assign(this.indicator.style, styles);
154+
}
155+
}
156+
157+
export function createTabHelpers(
158+
host: IgcTabsComponent,
159+
container: Ref<HTMLElement>,
160+
indicator: Ref<HTMLElement>
161+
) {
162+
return new TabsHelpers(host, container, indicator);
163+
}
164+
165+
export function getTabHeader(tab: IgcTabComponent): HTMLElement {
166+
return tab.renderRoot.querySelector('[part~="tab-header"]')!;
167+
}

src/components/tabs/tab-panel.ts

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)