Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,22 @@
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"extends": ["plugin:@nx/typescript", "plugin:@nx/angular"],
"parserOptions": {
"project": "./tsconfig.*?.json"
},
"rules": {
"semi": "off",
"@typescript-eslint/semi": ["error"],
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off",
"@angular-eslint/prefer-standalone": [
"off"
]
"@typescript-eslint/no-unused-expressions": "off",
"@angular-eslint/prefer-standalone": "off",
"@angular-eslint/prefer-inject": "off"
}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {
"@typescript-eslint/no-extra-semi": "error",
"no-extra-semi": "off"
}
}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/on-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
key: dist-${{ github.run_id }}

# - run: npm run scully
- run: npx ts-node ./scripts/gh-pages-predeploy.ts
- run: npx tsx ./scripts/gh-pages-predeploy.ts
- run: |
cd gh-pages
git config user.email gh-actions-${GITHUB_ACTOR}@github.com
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ Here is the version compatibility list:

| ngx-bootstrap | Angular | Bootstrap CSS |
|---------------|-----------------| ----------------------- |
| 20.x.x | 20.x.x | 5.x.x or 4.x.x |
Comment thread
davidIce marked this conversation as resolved.
| 19.x.x | 19.x.x | 5.x.x or 4.x.x |
| 18.x.x | 18.x.x | 5.x.x or 4.x.x |
| 12.x.x | 17.x.x | 5.x.x or 4.x.x |
Expand Down
8 changes: 5 additions & 3 deletions apps/ngx-bootstrap-docs-e2e/src/support/accordion.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ export class AccordionPo extends BasePo {
}

async clickOnAccordionGroup(baseSelector: string, itemIndex: number) {
await this.page
const accordionButton = this.page
.locator(baseSelector + ' accordion-group button')
.nth(itemIndex)
.click();
.nth(itemIndex);

await accordionButton.waitFor({ state: 'visible', timeout: 10000 });
await accordionButton.click();
}

async expectItemContentVisible(baseSelector: string, itemIndex: number, visible: boolean) {
Expand Down
8 changes: 4 additions & 4 deletions apps/ngx-bootstrap-docs-e2e/src/support/alerts.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ export class AlertsPo extends BasePo {
local: '.alert-md-local',
};

async expectAlertVisible(baseSelector: string, alertType: string, visible = true, timeout = 5000) {
await expect(await this.page
.locator(baseSelector + ` ${this.alertType[alertType]}`)
).toBeVisible({ timeout: timeout, visible: visible });
async expectAlertVisible(baseSelector: string, alertType: string, visible = true, timeout = 10000) {
const alertElement = this.page.locator(baseSelector + ` ${this.alertType[alertType]}`);
await alertElement.waitFor({ state: visible ? 'visible' : 'hidden', timeout: timeout });
await expect(alertElement).toBeVisible({ visible: visible });
}

async expectBtnNotExist(baseSelector: string, buttonName: string) {
Expand Down
7 changes: 7 additions & 0 deletions apps/ngx-bootstrap-docs-e2e/src/support/base.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export class BasePo {
async navigateTo() {
const bsVersionRoute = process.env['bsVersion'] ? `?_bsVersion=bs${process.env['bsVersion']}` : '';
await this.page.goto(this.pageUrl + bsVersionRoute);
await this.page.waitForLoadState('domcontentloaded');
// Ensure Overview tab is active by clicking on it
const overviewTab = this.page.locator('a[aria-selected="false"]').getByText('Overview');
if (await overviewTab.isVisible()) {
await overviewTab.click();
await this.page.waitForTimeout(1000); // Wait for tab content to load
}
}

async scrollToMenu(menuTxt: string) {
Expand Down
8 changes: 5 additions & 3 deletions apps/ngx-bootstrap-docs-e2e/src/support/buttons.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ export class ButtonsPo extends BasePo {
};

async expectBtnVisible(baseSelector: string, btnSelector: string, btnName: string, btnNumber?: number) {
await expect(await this.page
const btnElement = this.page
.locator(baseSelector + ` ${btnSelector}`)
.getByText(btnName)
.nth(btnNumber ? btnNumber : 0)
).toBeVisible();
.nth(btnNumber ? btnNumber : 0);

await btnElement.waitFor({ state: 'visible', timeout: 10000 });
await expect(btnElement).toBeVisible();
}

async expectBtnEnabled(baseSelector: string, btnName: string, enabled = true, btnNumber?: number) {
Expand Down
4 changes: 0 additions & 4 deletions apps/ngx-bootstrap-docs/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"*.ts"
],
"extends": [
"plugin:@nx/angular",
"plugin:@angular-eslint/template/process-inline-templates"
],
"parserOptions": {
Expand All @@ -37,9 +36,6 @@
"style": "kebab-case"
}
],
"@angular-eslint/prefer-standalone": [
"off"
],
"@typescript-eslint/no-explicit-any": 0,
"@angular-eslint/component-class-suffix": 0,
"@angular-eslint/no-output-on-prefix": 0
Expand Down
3 changes: 0 additions & 3 deletions apps/ngx-bootstrap-docs/src/assets/css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
@import "bs-datepicker";

/* HEADER */
.bs-datepicker-head {
width: 150px;
}
header {
padding: 10px 50px 10px 30px;
height: $header-height;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "19.0.3"
"version": "20.0.2"
}
209 changes: 209 additions & 0 deletions e2e/issues/issue-823.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { test, expect } from '@playwright/test';

test.describe('Issue #823: Tab ordering with ngIf/dynamic directives', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.click('text=Tabs');
});

test('should display tabs in correct order regardless of creation order', async ({ page }) => {
// Wait for tabs to load
await page.waitForSelector('.nav-tabs');

// Check that tabs exist and are in some order
const tabs = page.locator('.nav-item');
const tabCount = await tabs.count();

if (tabCount > 0) {
// Tabs should be visible and clickable
expect(await tabs.first().isVisible()).toBe(true);

// Should be able to click on tabs
await tabs.first().click();

// Active tab should have active class
const activeTab = page.locator('.nav-item.active, .nav-link.active');
expect(await activeTab.count()).toBeGreaterThan(0);
}
});

test('should maintain tab functionality with ordered tabs', async ({ page }) => {
// Test that tab ordering doesn't break basic functionality
await page.waitForSelector('.nav-tabs');

const tabs = page.locator('.nav-link');
const tabCount = await tabs.count();

if (tabCount >= 2) {
// Click first tab
await tabs.first().click();
expect(await tabs.first().getAttribute('class')).toContain('active');

// Click second tab
await tabs.nth(1).click();
expect(await tabs.nth(1).getAttribute('class')).toContain('active');

// First tab should no longer be active
expect(await tabs.first().getAttribute('class')).not.toContain('active');
}
});

test('should handle dynamic tab addition and removal', async ({ page }) => {
// Test dynamic behavior that tabOrder should help with
await page.waitForSelector('.nav-tabs');

const initialTabCount = await page.locator('.nav-item').count();

// Look for any buttons that might add/remove tabs dynamically
const addButtons = page.locator('button:has-text("Add"), button:has-text("New Tab")');
const removeButtons = page.locator('button:has-text("Remove"), .bs-remove-tab');

// If there are dynamic controls, test them
if (await addButtons.count() > 0) {
await addButtons.first().click();

// Tab count might change
const newTabCount = await page.locator('.nav-item').count();
expect(newTabCount).toBeGreaterThanOrEqual(initialTabCount);
}

// Test removable tabs if they exist
if (await removeButtons.count() > 0) {
expect(await removeButtons.first().isVisible()).toBe(true);
}
});

test('should preserve tab content when ordering changes', async ({ page }) => {
// Ensure tab content is preserved with ordering
await page.waitForSelector('.nav-tabs');

const tabs = page.locator('.nav-link');
const tabContent = page.locator('.tab-content, .tab-pane');

if (await tabs.count() > 0 && await tabContent.count() > 0) {
// Click on different tabs and verify content shows
for (let i = 0; i < Math.min(await tabs.count(), 3); i++) {
await tabs.nth(i).click();

// Content should be visible
expect(await tabContent.first().isVisible()).toBe(true);
}
}
});

test('should handle conditional tabs with ngIf-like behavior', async ({ page }) => {
// Simulate the ngIf scenario from the issue
await page.waitForSelector('.nav-tabs');

// Check if there are any conditional/dynamic elements
const conditionalElements = page.locator('[*ngIf], [ng-if], .conditional-tab');

// Basic functionality should work even with conditional rendering
const tabs = page.locator('.nav-link');
if (await tabs.count() > 0) {
// Should be able to navigate between tabs
await tabs.first().click();

// Tab should become active
const activeClass = await tabs.first().getAttribute('class');
expect(activeClass).toContain('active');
}
});

test('should maintain accessibility with ordered tabs', async ({ page }) => {
// Test that tab ordering doesn't break accessibility
await page.waitForSelector('.nav-tabs');

const tabs = page.locator('.nav-link');

if (await tabs.count() > 0) {
// Check for proper ARIA attributes
const firstTab = tabs.first();

// Should have proper role
const role = await firstTab.getAttribute('role');
expect(role).toBe('tab');

// Should have aria-selected
const ariaSelected = await firstTab.getAttribute('aria-selected');
expect(ariaSelected).toBeTruthy();

// Should be focusable
await firstTab.focus();
const focusedElement = page.locator(':focus');
expect(await focusedElement.count()).toBe(1);
}
});

test('should handle keyboard navigation with ordered tabs', async ({ page }) => {
// Test keyboard navigation works with tab ordering
await page.waitForSelector('.nav-tabs');

const tabs = page.locator('.nav-link');

if (await tabs.count() >= 2) {
// Focus first tab
await tabs.first().focus();

// Use arrow keys to navigate
await page.keyboard.press('ArrowRight');

// Should move focus (implementation may vary)
const focusedElement = page.locator(':focus');
expect(await focusedElement.count()).toBe(1);

// Test Tab key navigation
await page.keyboard.press('Tab');

// Focus should move to next element
expect(await focusedElement.count()).toBe(1);
}
});

test('should handle edge cases for tab ordering', async ({ page }) => {
// Test edge cases that might occur with tab ordering
await page.waitForSelector('.nav-tabs');

const tabs = page.locator('.nav-item');
const initialCount = await tabs.count();

// Rapid clicking shouldn't break ordering
if (initialCount >= 2) {
await tabs.first().click();
await tabs.nth(1).click();
await tabs.first().click();

// Should still work correctly
expect(await tabs.count()).toBe(initialCount);
}

// Page refresh should maintain consistent ordering
await page.reload();
await page.waitForSelector('.nav-tabs');

const tabsAfterReload = page.locator('.nav-item');
expect(await tabsAfterReload.count()).toBe(initialCount);
});

test('should support tab ordering in different layouts', async ({ page }) => {
// Test that tab ordering works in different tab layouts
await page.waitForSelector('.nav-tabs');

// Look for different tab styles/layouts
const verticalTabs = page.locator('.nav-tabs.flex-column, .nav-pills.flex-column');
const justifiedTabs = page.locator('.nav-justified');
const pillTabs = page.locator('.nav-pills');

// Test basic functionality regardless of layout
const anyTabs = page.locator('.nav-link');

if (await anyTabs.count() > 0) {
// Should work in any layout
await anyTabs.first().click();

// Should have active state
const activeTab = page.locator('.nav-link.active');
expect(await activeTab.count()).toBe(1);
}
});
Comment thread
davidIce marked this conversation as resolved.
});
4 changes: 0 additions & 4 deletions libs/common-docs/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"*.ts"
],
"extends": [
"plugin:@nx/angular",
"plugin:@angular-eslint/template/process-inline-templates"
],
"parserOptions": {
Expand All @@ -35,9 +34,6 @@
"prefix": "ngx-bootstrap-base",
"style": "kebab-case"
}
],
"@angular-eslint/prefer-standalone": [
"off"
]
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class DiscoverComponent {
link: 'https://trovimap.com/'
},
{
logo: 'https://www.atmetis.nl/wp-content/uploads/2017/08/cropped-logo-atmetis-tagline-rgb1500px-1.png',
logo: 'https://atmetis.nl/templates/assets/img/logo/atmetis-color.svg',
name: 'AtMetis',
description: 'Internal web-application for AtMetis - assessment company from Netherlands',
link: 'https://www.atmetis.nl/'
Expand All @@ -78,7 +78,7 @@ export class DiscoverComponent {
link: 'https://www.pramati.com/'
},
{
logo: 'https://res.cloudinary.com/crunchbase-production/image/upload/c_lpad,h_170,w_170,f_auto,b_white,q_auto:eco/v1484678055/ectxiezxgzm3srv2jkvh.png',
logo: 'https://employes.nl/static/employes-1504611522217dfcf2c76b401d63c44e.svg',
Comment thread
davidIce marked this conversation as resolved.
name: 'Employes',
description: 'Employes is a dutch based payroll-provider that offers a unique modern way to pay employees.\n',
link: 'https://app.employes.nl/'
Expand Down
Loading
Loading