diff --git a/core/src/components/col/col.tsx b/core/src/components/col/col.tsx index a5d3614828f..dc5694f16b0 100644 --- a/core/src/components/col/col.tsx +++ b/core/src/components/col/col.tsx @@ -1,6 +1,7 @@ import type { ComponentInterface } from '@stencil/core'; -import { Component, Host, Listen, Prop, forceUpdate, h } from '@stencil/core'; +import { Component, Element, Host, Listen, Prop, forceUpdate, h } from '@stencil/core'; import { matchBreakpoint } from '@utils/media'; +import { isRTL } from '@utils/rtl'; import { getIonMode } from '../../global/ionic-global'; @@ -15,6 +16,8 @@ const BREAKPOINTS = ['', 'xs', 'sm', 'md', 'lg', 'xl']; shadow: true, }) export class Col implements ComponentInterface { + @Element() el!: HTMLElement; + /** * The amount to offset the column, in terms of how many columns it should shift to the end * of the total available. @@ -235,20 +238,20 @@ export class Col implements ComponentInterface { }; } - private calculateOffset(isRTL: boolean) { - return this.calculatePosition('offset', isRTL ? 'margin-right' : 'margin-left'); + private calculateOffset(rtl: boolean) { + return this.calculatePosition('offset', rtl ? 'margin-right' : 'margin-left'); } - private calculatePull(isRTL: boolean) { - return this.calculatePosition('pull', isRTL ? 'left' : 'right'); + private calculatePull(rtl: boolean) { + return this.calculatePosition('pull', rtl ? 'left' : 'right'); } - private calculatePush(isRTL: boolean) { - return this.calculatePosition('push', isRTL ? 'right' : 'left'); + private calculatePush(rtl: boolean) { + return this.calculatePosition('push', rtl ? 'right' : 'left'); } render() { - const isRTL = document.dir === 'rtl'; + const rtl = isRTL(this.el); const mode = getIonMode(this); return ( diff --git a/core/src/components/item-options/item-options.tsx b/core/src/components/item-options/item-options.tsx index d495580d7a5..2a1dd684487 100644 --- a/core/src/components/item-options/item-options.tsx +++ b/core/src/components/item-options/item-options.tsx @@ -36,7 +36,7 @@ export class ItemOptions implements ComponentInterface { render() { const mode = getIonMode(this); - const isEnd = isEndSide(this.side); + const isEnd = isEndSide(this.side, this.el); return ( 0; @@ -260,7 +261,7 @@ export class ItemSliding implements ComponentInterface { this.leftOptions = this.rightOptions = undefined; for (const option of options) { - const side = isEndSide(option.side ?? option.getAttribute('side')) ? 'end' : 'start'; + const side = isEndSide(option.side ?? option.getAttribute('side'), option) ? 'end' : 'start'; if (side === 'start') { this.leftOptions = option; @@ -280,7 +281,7 @@ export class ItemSliding implements ComponentInterface { * do not open left side so swipe to go * back will still work. */ - const rtl = document.dir === 'rtl'; + const rtl = isRTL(this.el); const atEdge = rtl ? window.innerWidth - gesture.startX < 15 : gesture.startX < 15; if (atEdge) { return false; diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx index fa7de421fc2..4f9e1b554b4 100644 --- a/core/src/components/item/item.tsx +++ b/core/src/components/item/item.tsx @@ -4,6 +4,7 @@ import type { AttributeController } from '@utils/attribute-controller'; import { createAttributeController } from '@utils/attribute-controller'; import type { AnchorInterface, ButtonInterface } from '@utils/element-interface'; import { raf } from '@utils/helpers'; +import { isRTL } from '@utils/rtl'; import { createColorClasses, hostContext, openURL } from '@utils/theme'; import { chevronForward } from 'ionicons/icons'; @@ -473,7 +474,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac 'item-focus-indicator-room': slottedIndicatorNeedsRoom, 'ion-activatable': canActivate, 'ion-focusable': this.focusable, - 'item-rtl': document.dir === 'rtl', + 'item-rtl': isRTL(this.el), }), }} role={inList ? 'listitem' : null} diff --git a/core/src/components/label/label.tsx b/core/src/components/label/label.tsx index cea7a844ec4..30859a1003d 100644 --- a/core/src/components/label/label.tsx +++ b/core/src/components/label/label.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core'; +import { isRTL } from '@utils/rtl'; import { createColorClasses, hostContext } from '@utils/theme'; import { getIonMode } from '../../global/ionic-global'; @@ -112,7 +113,7 @@ export class Label implements ComponentInterface { 'in-item-color': hostContext('ion-item.ion-color', this.el), [`label-${position}`]: position !== undefined, [`label-no-animate`]: this.noAnimate, - 'label-rtl': document.dir === 'rtl', + 'label-rtl': isRTL(this.el), })} > diff --git a/core/src/components/popover/animations/ios.enter.ts b/core/src/components/popover/animations/ios.enter.ts index 4273074f6bb..c8143ab098f 100644 --- a/core/src/components/popover/animations/ios.enter.ts +++ b/core/src/components/popover/animations/ios.enter.ts @@ -1,5 +1,6 @@ import { createAnimation } from '@utils/animation/animation'; import { getElementRoot } from '@utils/helpers'; +import { isRTL } from '@utils/rtl'; import type { Animation } from '../../../interface'; import { @@ -31,7 +32,7 @@ const POPOVER_IOS_MIN_EDGE_MARGIN = 25; export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => { const { event: ev, size, trigger, reference, side, align } = opts; const doc = baseEl.ownerDocument as any; - const isRTL = doc.dir === 'rtl'; + const rtl = isRTL(baseEl); const root = getElementRoot(baseEl); const contentEl = root.querySelector('.popover-content') as HTMLElement; const arrowEl = root.querySelector('.popover-arrow') as HTMLElement | null; @@ -61,12 +62,12 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => const defaultPosition = { top: bodyHeight / 2 - contentHeight / 2, left: bodyWidth / 2 - contentWidth / 2, - originX: isRTL ? 'right' : 'left', + originX: rtl ? 'right' : 'left', originY: 'top', }; const results = getPopoverPosition( - isRTL, + rtl, contentWidth, contentHeight, arrowWidth, diff --git a/core/src/components/popover/animations/md.enter.ts b/core/src/components/popover/animations/md.enter.ts index 6d37474ceb2..09e59471538 100644 --- a/core/src/components/popover/animations/md.enter.ts +++ b/core/src/components/popover/animations/md.enter.ts @@ -1,5 +1,6 @@ import { createAnimation } from '@utils/animation/animation'; import { getElementRoot } from '@utils/helpers'; +import { isRTL } from '@utils/rtl'; import type { Animation } from '../../../interface'; import { @@ -19,7 +20,7 @@ const POPOVER_MD_BODY_PADDING = 12; export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => { const { event: ev, size, trigger, reference, side, align } = opts; const doc = baseEl.ownerDocument as any; - const isRTL = doc.dir === 'rtl'; + const rtl = isRTL(baseEl); const root = getElementRoot(baseEl); const contentEl = root.querySelector('.popover-content') as HTMLElement; @@ -48,12 +49,12 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => const defaultPosition = { top: bodyHeight / 2 - contentHeight / 2, left: bodyWidth / 2 - contentWidth / 2, - originX: isRTL ? 'right' : 'left', + originX: rtl ? 'right' : 'left', originY: 'top', }; const results = getPopoverPosition( - isRTL, + rtl, contentWidth, contentHeight, 0, diff --git a/core/src/components/progress-bar/progress-bar.tsx b/core/src/components/progress-bar/progress-bar.tsx index 12ccd66dec9..02b587388f7 100644 --- a/core/src/components/progress-bar/progress-bar.tsx +++ b/core/src/components/progress-bar/progress-bar.tsx @@ -1,6 +1,7 @@ import type { ComponentInterface } from '@stencil/core'; -import { Component, Host, Prop, h } from '@stencil/core'; +import { Component, Element, Host, Prop, h } from '@stencil/core'; import { clamp } from '@utils/helpers'; +import { isRTL } from '@utils/rtl'; import { createColorClasses } from '@utils/theme'; import { config } from '../../global/config'; @@ -24,6 +25,8 @@ import type { Color } from '../../interface'; shadow: true, }) export class ProgressBar implements ComponentInterface { + @Element() el!: HTMLElement; + /** * The state of the progress bar, based on if the time the process takes is known or not. * Default options are: `"determinate"` (no animation), `"indeterminate"` (animate from left to right). @@ -71,7 +74,7 @@ export class ProgressBar implements ComponentInterface { [mode]: true, [`progress-bar-${type}`]: true, 'progress-paused': paused, - 'progress-bar-reversed': document.dir === 'rtl' ? !reversed : reversed, + 'progress-bar-reversed': isRTL(this.el) ? !reversed : reversed, 'progress-bar-solid': progressSolid, })} > diff --git a/core/src/components/title/title.tsx b/core/src/components/title/title.tsx index 82a108071ff..40b30b63820 100644 --- a/core/src/components/title/title.tsx +++ b/core/src/components/title/title.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Prop, Watch, h } from '@stencil/core'; +import { isRTL } from '@utils/rtl'; import { createColorClasses } from '@utils/theme'; import { getIonMode } from '../../global/ionic-global'; @@ -64,7 +65,7 @@ export class ToolbarTitle implements ComponentInterface { class={createColorClasses(this.color, { [mode]: true, [`title-${size}`]: true, - 'title-rtl': document.dir === 'rtl', + 'title-rtl': isRTL(this.el), })} >
diff --git a/core/src/utils/helpers.spec.ts b/core/src/utils/helpers.spec.ts index aaa4b7dbdb7..47781276a3e 100644 --- a/core/src/utils/helpers.spec.ts +++ b/core/src/utils/helpers.spec.ts @@ -6,16 +6,6 @@ describe('isEndSide', () => { document.body.innerHTML = ''; }); - it('should use document direction when no host element is provided', () => { - document.dir = 'ltr'; - expect(isEndSide('start')).toBe(false); - expect(isEndSide('end')).toBe(true); - - document.dir = 'rtl'; - expect(isEndSide('start')).toBe(true); - expect(isEndSide('end')).toBe(false); - }); - // https://github.com/ionic-team/ionic-framework/issues/30226 it('should use the nearest ancestor dir attribute', () => { document.dir = 'ltr'; diff --git a/core/src/utils/helpers.ts b/core/src/utils/helpers.ts index cd10c0f0881..65fdf4e950c 100644 --- a/core/src/utils/helpers.ts +++ b/core/src/utils/helpers.ts @@ -374,10 +374,9 @@ export const pointerCoord = (ev: any): { x: number; y: number } => { * * @param side The current side before being redefined based on the direction. * @param hostEl The component's host element. The direction is resolved from - * it or its nearest ancestor that declares one. When omitted, the direction - * is resolved from the document. + * it or its nearest ancestor that declares one. */ -export const isEndSide = (side: Side, hostEl?: HTMLElement): boolean => { +export const isEndSide = (side: Side, hostEl: HTMLElement): boolean => { const rtl = isRTL(hostEl); switch (side) { diff --git a/core/src/utils/input-shims/hacks/common.ts b/core/src/utils/input-shims/hacks/common.ts index 7553c07e103..65c922dac04 100644 --- a/core/src/utils/input-shims/hacks/common.ts +++ b/core/src/utils/input-shims/hacks/common.ts @@ -1,3 +1,5 @@ +import { isRTL } from '@utils/rtl'; + const cloneMap = new WeakMap(); export const relocateInput = ( @@ -73,10 +75,9 @@ const addClone = ( * Position the clone at the same horizontal offset as the native input * to prevent the placeholder from overlapping start slot content (e.g., icons). */ - const doc = componentEl.ownerDocument!; - const isRTL = doc.dir === 'rtl'; + const rtl = isRTL(componentEl); - if (isRTL) { + if (rtl) { const parentWidth = (parentEl as HTMLElement).offsetWidth; const startOffset = parentWidth - inputEl.offsetLeft - inputEl.offsetWidth; clonedEl.style.insetInlineStart = `${startOffset}px`; @@ -87,7 +88,7 @@ const addClone = ( parentEl.appendChild(clonedEl); cloneMap.set(componentEl, clonedEl); - const tx = isRTL ? 9999 : -9999; + const tx = rtl ? 9999 : -9999; componentEl.style.pointerEvents = 'none'; inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0) scale(0)`; }; diff --git a/core/src/utils/rtl/dir.spec.ts b/core/src/utils/rtl/dir.spec.ts index cc3245e0f55..7d65b6c3c79 100644 --- a/core/src/utils/rtl/dir.spec.ts +++ b/core/src/utils/rtl/dir.spec.ts @@ -75,20 +75,8 @@ describe('rtl: dir', () => { // value left behind by another test. expect(document.dir).toBe(''); - expect(isRTL()).toBe(false); - expect(isRTL(null)).toBe(false); expect(isRTL(document.createElement('div'))).toBe(false); expect(isRTL(render('
'))).toBe(false); }); }); - - describe('without a host element', () => { - it('should use the document dir', () => { - document.dir = 'rtl'; - expect(isRTL()).toBe(true); - - document.dir = 'ltr'; - expect(isRTL()).toBe(false); - }); - }); }); diff --git a/core/src/utils/rtl/dir.ts b/core/src/utils/rtl/dir.ts index 1d7de2dd261..4f9ba10a430 100644 --- a/core/src/utils/rtl/dir.ts +++ b/core/src/utils/rtl/dir.ts @@ -9,8 +9,8 @@ * * @param hostEl the element to resolve the direction for. */ -export const isRTL = (hostEl?: Element | null): boolean => { - for (let el = hostEl; el; el = el.parentElement) { +export const isRTL = (hostEl: Element): boolean => { + for (let el: Element | null = hostEl; el; el = el.parentElement) { const dir = el.getAttribute('dir')?.toLowerCase(); if (dir === 'rtl') { @@ -20,5 +20,5 @@ export const isRTL = (hostEl?: Element | null): boolean => { return false; } } - return document?.dir?.toLowerCase() === 'rtl'; + return hostEl.ownerDocument?.dir?.toLowerCase() === 'rtl'; }; diff --git a/core/src/utils/transition/ios.transition.ts b/core/src/utils/transition/ios.transition.ts index 45a9c864b7c..37dce8bfe10 100644 --- a/core/src/utils/transition/ios.transition.ts +++ b/core/src/utils/transition/ios.transition.ts @@ -1,3 +1,5 @@ +import { isRTL } from '@utils/rtl'; + import type { Animation } from '../../interface'; import { createAnimation } from '../animation/animation'; import type { TransitionOptions } from '../transition'; @@ -501,9 +503,9 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio const CENTER = '0%'; const OFF_OPACITY = 0.8; - const isRTL = navEl.ownerDocument.dir === 'rtl'; - const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%'; - const OFF_LEFT = isRTL ? '33%' : '-33%'; + const rtl = isRTL(navEl); + const OFF_RIGHT = rtl ? '-99.5%' : '99.5%'; + const OFF_LEFT = rtl ? '33%' : '-33%'; const enteringEl = opts.enteringEl; const leavingEl = opts.leavingEl; @@ -584,13 +586,7 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio const enteringContentHasLargeTitle = enteringEl.querySelector('ion-header.header-collapse-condense'); - const { forward, backward } = createLargeTitleTransition( - rootAnimation, - isRTL, - backDirection, - enteringEl, - leavingEl - ); + const { forward, backward } = createLargeTitleTransition(rootAnimation, rtl, backDirection, enteringEl, leavingEl); enteringToolBarEls.forEach((enteringToolBarEl) => { const enteringToolBar = createAnimation(); enteringToolBar.addElement(enteringToolBarEl); @@ -668,7 +664,7 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio if (!translucentHeader) { enteringToolBarBg.fromTo(OPACITY, 0.01, 'var(--opacity)'); } else { - enteringToolBarBg.fromTo('transform', isRTL ? 'translateX(-100%)' : 'translateX(100%)', 'translateX(0px)'); + enteringToolBarBg.fromTo('transform', rtl ? 'translateX(-100%)' : 'translateX(100%)', 'translateX(0px)'); } // forward direction, entering page has a back button @@ -680,7 +676,7 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio const enteringBackBtnText = createAnimation(); enteringBackBtnText .addElement(shadow(backButtonEl).querySelector('.button-text')!) // REVIEW - .fromTo(`transform`, isRTL ? 'translateX(-100px)' : 'translateX(100px)', 'translateX(0px)'); + .fromTo(`transform`, rtl ? 'translateX(-100px)' : 'translateX(100px)', 'translateX(0px)'); enteringToolBar.addAnimation(enteringBackBtnText); } @@ -709,7 +705,7 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio // leaving content, back direction leavingContent .beforeClearStyles([OPACITY]) - .fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)'); + .fromTo('transform', `translateX(${CENTER})`, rtl ? 'translateX(-100%)' : 'translateX(100%)'); const leavingPage = getIonPageElement(leavingEl) as HTMLElement; rootAnimation.afterAddWrite(() => { @@ -811,14 +807,14 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio if (!inactiveHeader) { // leaving toolbar, back direction leavingTitle - .fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)') + .fromTo('transform', `translateX(${CENTER})`, rtl ? 'translateX(-100%)' : 'translateX(100%)') .fromTo(OPACITY, 0.99, 0); } leavingToolBarItems.fromTo( 'transform', `translateX(${CENTER})`, - isRTL ? 'translateX(-100%)' : 'translateX(100%)' + rtl ? 'translateX(-100%)' : 'translateX(100%)' ); leavingToolBarBg.beforeClearStyles([OPACITY, 'transform']); // leaving toolbar, back direction, and there's no entering toolbar @@ -827,14 +823,14 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio if (!translucentHeader) { leavingToolBarBg.fromTo(OPACITY, 'var(--opacity)', 0); } else { - leavingToolBarBg.fromTo('transform', 'translateX(0px)', isRTL ? 'translateX(-100%)' : 'translateX(100%)'); + leavingToolBarBg.fromTo('transform', 'translateX(0px)', rtl ? 'translateX(-100%)' : 'translateX(100%)'); } if (backButtonEl && !backward) { const leavingBackBtnText = createAnimation(); leavingBackBtnText .addElement(shadow(backButtonEl).querySelector('.button-text')!) // REVIEW - .fromTo('transform', `translateX(${CENTER})`, `translateX(${(isRTL ? -124 : 124) + 'px'})`); + .fromTo('transform', `translateX(${CENTER})`, `translateX(${(rtl ? -124 : 124) + 'px'})`); leavingToolBar.addAnimation(leavingBackBtnText); } } else { diff --git a/docs/component-guide.md b/docs/component-guide.md index f685fd6163d..1b3094daf59 100644 --- a/docs/component-guide.md +++ b/docs/component-guide.md @@ -806,9 +806,11 @@ These mixins depend on the `:host-context` pseudo-class when used inside of shad To work around this, you should set an RTL class on the host of your component and set your RTL styles by targeting that class: ```tsx +import { isRTL } from '@utils/rtl'; + ...