diff --git a/projects/coreui-angular/src/lib/chip-set/chip-set.component.spec.ts b/projects/coreui-angular/src/lib/chip-set/chip-set.component.spec.ts
index 006e8198..0051eef1 100644
--- a/projects/coreui-angular/src/lib/chip-set/chip-set.component.spec.ts
+++ b/projects/coreui-angular/src/lib/chip-set/chip-set.component.spec.ts
@@ -101,6 +101,7 @@ describe('ChipSetComponent', () => {
});
afterEach(() => {
+ document.documentElement.removeAttribute('dir');
vi.restoreAllMocks();
});
@@ -198,6 +199,19 @@ describe('ChipSetComponent', () => {
expect(document.activeElement).toBe(chip('a'));
});
+ it('ChipSet keeps arrow keys in an LTR island on an RTL page', async () => {
+ document.documentElement.dir = 'rtl';
+ host.selectable.set(true);
+ host.dir.set('ltr');
+ host.declare('a', 'b');
+ await fixture.whenStable();
+ chip('a').focus();
+ press(chip('a'), 'ArrowRight');
+ expect(document.activeElement).toBe(chip('b'));
+ press(chip('b'), 'ArrowLeft');
+ expect(document.activeElement).toBe(chip('a'));
+ });
+
it('ChipSet fires remove so the parent can drop the chip', async () => {
host.removable.set(true);
host.chips.set(['a', 'b']);
diff --git a/projects/coreui-angular/src/lib/services/rtl.service.spec.ts b/projects/coreui-angular/src/lib/services/rtl.service.spec.ts
index 1274c3ea..a2c93d1a 100644
--- a/projects/coreui-angular/src/lib/services/rtl.service.spec.ts
+++ b/projects/coreui-angular/src/lib/services/rtl.service.spec.ts
@@ -4,13 +4,139 @@ import { RtlService } from './rtl.service';
describe('RtlService', () => {
let service: RtlService;
+ let fixtureEl: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(RtlService);
+ fixtureEl = document.createElement('div');
+ document.body.append(fixtureEl);
+ });
+
+ afterEach(() => {
+ fixtureEl.remove();
+ document.documentElement.removeAttribute('dir');
+ document.body.removeAttribute('dir');
+ vi.restoreAllMocks();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
+
+ it('should read the direction of the element, not of the document', () => {
+ fixtureEl.innerHTML = '
';
+
+ expect(service.isRTL(fixtureEl.querySelector('#inside'))).toBe(true);
+ expect(service.isRTL(fixtureEl.querySelector('#outside'))).toBe(false);
+ });
+
+ it('should leave an element that sets its own direction out of an RTL subtree', () => {
+ document.documentElement.dir = 'rtl';
+ fixtureEl.innerHTML = '
';
+
+ expect(service.isRTL(fixtureEl.querySelector('#island'))).toBe(false);
+ });
+
+ it('should fall back to the document when given nothing', () => {
+ document.documentElement.dir = 'rtl';
+ expect(service.isRTL()).toBe(true);
+
+ document.documentElement.dir = '';
+ expect(service.isRTL()).toBe(false);
+ });
+
+ it('should answer the same for a detached element as for a connected one under an explicit dir', () => {
+ const cases: [string, boolean][] = [
+ ['
', true],
+ ['
', false]
+ ];
+
+ for (const [markup, expected] of cases) {
+ const detached = document.createElement('div');
+ detached.innerHTML = markup;
+
+ expect(service.isRTL(detached.querySelector('[data-probe]'))).toBe(expected);
+
+ fixtureEl.append(detached);
+
+ expect(service.isRTL(detached.querySelector('[data-probe]'))).toBe(expected);
+ }
+ });
+
+ it('should answer for the document when a detached element declares nothing', () => {
+ const detached = document.createElement('div');
+ detached.style.direction = 'rtl';
+ detached.innerHTML = '';
+
+ expect(service.isRTL(detached.firstElementChild as HTMLElement)).toBe(false);
+
+ document.documentElement.dir = 'rtl';
+
+ expect(service.isRTL(document.createElement('div'))).toBe(true);
+ });
+
+ it('should answer for an element that is not in the document yet', () => {
+ const detached = document.createElement('div');
+ detached.dir = 'rtl';
+ detached.innerHTML = '';
+
+ expect(service.isRTL(detached.firstElementChild as HTMLElement)).toBe(true);
+ });
+
+ it('should answer a detached element in a browser without :dir()', () => {
+ const matches = Element.prototype.matches;
+ vi.spyOn(Element.prototype, 'matches').mockImplementation(function (this: Element, selectors: string) {
+ if (selectors.includes(':dir(')) {
+ throw new SyntaxError(`'${selectors}' is not a valid selector`);
+ }
+ return matches.call(this, selectors);
+ });
+ const detached = document.createElement('div');
+ detached.innerHTML = '
hello
';
+
+ expect(service.isRTL(detached.querySelector('#probe'))).toBe(true);
+ });
+
+ it('should resolve dir=auto from the content it has at the time', () => {
+ fixtureEl.innerHTML = '
مرحبا
';
+ const probe = fixtureEl.querySelector('#probe');
+
+ expect(service.isRTL(probe)).toBe(true);
+
+ probe?.parentElement?.firstChild?.replaceWith('hello');
+
+ expect(service.isRTL(probe)).toBe(false);
+ });
+
+ it('should follow a direction set from CSS, which is what the layout follows', () => {
+ fixtureEl.innerHTML =
+ '
' +
+ '
';
+
+ expect(service.isRTL(fixtureEl.querySelector('#rtl-by-css'))).toBe(true);
+ expect(service.isRTL(fixtureEl.querySelector('#ltr-by-css'))).toBe(false);
+ });
+
+ it('should answer for a field the stylesheet keeps left-to-right, not for its container', () => {
+ document.documentElement.dir = 'rtl';
+ fixtureEl.innerHTML =
+ '';
+ const field = fixtureEl.querySelector('#field');
+
+ expect(service.isRTL(field)).toBe(false);
+ expect(service.isRTL(field?.parentElement)).toBe(true);
+ });
+
+ it('should read the nearest explicit dir when there is no computed direction', () => {
+ vi.spyOn(window, 'getComputedStyle').mockReturnValue({ direction: '' } as CSSStyleDeclaration);
+ document.documentElement.dir = 'rtl';
+ fixtureEl.innerHTML =
+ '