Skip to content

Commit

Permalink
fix(tabs): Nested tabs selection behavior (#1459)
Browse files Browse the repository at this point in the history
  • Loading branch information
rkaraivanov authored Nov 1, 2024
1 parent 2724d89 commit d44888c
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 13 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added
- Carousel component select method overload accepting index [#1457](https://github.com/IgniteUI/igniteui-webcomponents/issues/1457)

### Fixed
- Tabs - nested tabs selection [#713](https://github.com/IgniteUI/igniteui-webcomponents/issues/713)

## [5.1.1] - 2024-10-28
### Fixed
- Library - internal import path for styles and public exports for themes
Expand Down
4 changes: 2 additions & 2 deletions src/components/button-group/button-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { watch } from '../common/decorators/watch.js';
import { registerComponent } from '../common/definitions/register.js';
import type { Constructor } from '../common/mixins/constructor.js';
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
import { findElementFromEventPath } from '../common/util.js';
import { findElementFromEventPath, last } from '../common/util.js';
import { styles } from './themes/group.base.css.js';
import { all } from './themes/group.js';
import { styles as shared } from './themes/shared/group/group.common.css.js';
Expand Down Expand Up @@ -62,7 +62,7 @@ export default class IgcButtonGroupComponent extends EventEmitterMixin<

const buttons = this.toggleButtons;
const idx = buttons.indexOf(
added.length ? added.at(-1)! : attributes.at(-1)!
added.length ? last(added).node : last(attributes)
);

for (const [i, button] of buttons.entries()) {
Expand Down
5 changes: 3 additions & 2 deletions src/components/carousel/carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ export default class IgcCarouselComponent extends EventEmitterMixin<
if (activeSlides.length <= 1) {
return;
}

const idx = this.slides.indexOf(last(added.length ? added : attributes));
const idx = this.slides.indexOf(
added.length ? last(added).node : last(attributes)
);

for (const [i, slide] of this.slides.entries()) {
if (slide.active && i !== idx) {
Expand Down
13 changes: 9 additions & 4 deletions src/components/common/controllers/mutation-observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ type MutationControllerCallback<T> = (
* an array of selector strings or a predicate function.
*/
type MutationControllerFilter<T> = string[] | ((node: T) => boolean);
type MutationDOMChange<T> = { target: Element; node: T };

type MutationChange<T> = {
/** Elements that have attribute(s) changes. */
attributes: T[];
/** Elements that have been added. */
added: T[];
added: MutationDOMChange<T>[];
/** Elements that have been removed. */
removed: T[];
removed: MutationDOMChange<T>[];
};

export type MutationControllerParams<T> = {
Expand Down Expand Up @@ -110,10 +111,14 @@ class MutationController<T> implements ReactiveController {
);
} else if (record.type === 'childList') {
changes.added.push(
...mutationFilter(Array.from(record.addedNodes) as T[], filter)
...mutationFilter(Array.from(record.addedNodes) as T[], filter).map(
(node) => ({ target: record.target as Element, node })
)
);
changes.removed.push(
...mutationFilter(Array.from(record.removedNodes) as T[], filter)
...mutationFilter(Array.from(record.removedNodes) as T[], filter).map(
(node) => ({ target: record.target as Element, node })
)
);
}
}
Expand Down
45 changes: 44 additions & 1 deletion src/components/tabs/tabs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ import {
homeKey,
spaceBar,
} from '../common/controllers/key-bindings.js';
import { first, last } from '../common/util.js';
import { simulateClick, simulateKeyboard } from '../common/utils.spec.js';

describe('Tabs component', () => {
// Helper functions
const getTabs = (tabs: IgcTabsComponent) =>
Array.from(tabs.querySelectorAll(IgcTabComponent.tagName));
Array.from(
tabs.querySelectorAll<IgcTabComponent>(
`:scope > ${IgcTabComponent.tagName}`
)
);

const getPanels = (tabs: IgcTabsComponent) =>
Array.from(tabs.querySelectorAll(IgcTabPanelComponent.tagName));
Expand Down Expand Up @@ -670,4 +675,42 @@ describe('Tabs component', () => {
expect(() => tabs.appendChild(tab)).not.to.throw();
});
});

describe('issue-713', () => {
it('Nested tabs selection', async () => {
const tabs = await fixture<IgcTabsComponent>(html`
<igc-tabs>
<igc-tab>1</igc-tab>
<igc-tab>2</igc-tab>
<igc-tab-panel>
Panel 1
<igc-tabs>
<igc-tab>1.1</igc-tab>
<igc-tab selected>1.2</igc-tab>
<igc-tab-panel>Panel 1.1</igc-tab-panel>
<igc-tab-panel>Panel 1.2</igc-tab-panel>
</igc-tabs>
</igc-tab-panel>
<igc-tab-panel>Panel 2</igc-tab-panel>
</igc-tabs>
`);

const nestedTabs = tabs.querySelector(IgcTabsComponent.tagName)!;

expect(getSelectedTab(tabs).textContent).to.equal('1');
expect(getSelectedTab(nestedTabs).textContent).to.equal('1.2');

simulateClick(first(getTabs(nestedTabs)));
await elementUpdated(tabs);

expect(getSelectedTab(tabs).textContent).to.equal('1');
expect(getSelectedTab(nestedTabs).textContent).to.equal('1.1');

simulateClick(last(getTabs(tabs)));
await elementUpdated(tabs);

expect(getSelectedTab(tabs).textContent).to.equal('2');
expect(getSelectedTab(nestedTabs).textContent).to.equal('1.1');
});
});
});
18 changes: 14 additions & 4 deletions src/components/tabs/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,27 @@ export default class IgcTabsComponent extends EventEmitterMixin<
private _mutationCallback({
changes: { attributes, added, removed },
}: MutationControllerParams<IgcTabComponent>) {
this.setSelectedTab(attributes.find((tab) => tab.selected));
const ownAttributes = attributes.filter(
(tab) => tab.closest(this.tagName) === this
);
const ownAdded = added.filter(
({ target }) => target.closest(this.tagName) === this
);
const ownRemoved = removed.filter(
({ target }) => target.closest(this.tagName) === this
);

this.setSelectedTab(ownAttributes.find((tab) => tab.selected));

if (removed.length || added.length) {
for (const tab of removed) {
if (ownRemoved.length || ownAdded.length) {
for (const { node: tab } of ownRemoved) {
this.resizeObserver?.unobserve(tab);
if (tab.selected && tab === this.activeTab) {
this.activeTab = undefined;
}
}

for (const tab of added) {
for (const { node: tab } of ownAdded) {
this.resizeObserver?.observe(tab);
if (tab.selected) {
this.setSelectedTab(tab);
Expand Down

0 comments on commit d44888c

Please sign in to comment.