Skip to content

Commit 66f517d

Browse files
fix(segment-view): scroll and select the right item when the component is in RTL context; (#30675)
Issue number: resolves [#30079](#30079) --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Segments (IonSegment and IonSegmentView) do not work if placed on a dir="rtl" context. If click on button, it won't slide content of the next segment. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - calculate scroll value having into consideration the dir value; ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> [Preview](https://ionic-framework-git-fw-6768-ionic1.vercel.app/src/components/segment-view/test/rtl) --------- Co-authored-by: Shane <[email protected]>
1 parent c339bc3 commit 66f517d

File tree

3 files changed

+272
-74
lines changed

3 files changed

+272
-74
lines changed

core/src/components/segment-view/segment-view.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
22
import { Component, Element, Event, Host, Listen, Method, Prop, State, h } from '@stencil/core';
3+
import { isRTL } from '@utils/rtl';
34

45
import type { SegmentViewScrollEvent } from './segment-view-interface';
56

@@ -39,7 +40,8 @@ export class SegmentView implements ComponentInterface {
3940
@Listen('scroll')
4041
handleScroll(ev: Event) {
4142
const { scrollLeft, scrollWidth, clientWidth } = ev.target as HTMLElement;
42-
const scrollRatio = scrollLeft / (scrollWidth - clientWidth);
43+
const max = scrollWidth - clientWidth;
44+
const scrollRatio = (isRTL(this.el) ? -1 : 1) * (scrollLeft / max);
4345

4446
this.ionSegmentViewScroll.emit({
4547
scrollRatio,
@@ -125,9 +127,11 @@ export class SegmentView implements ComponentInterface {
125127
this.resetScrollEndTimeout();
126128

127129
const contentWidth = this.el.offsetWidth;
130+
const offset = index * contentWidth;
131+
128132
this.el.scrollTo({
129133
top: 0,
130-
left: index * contentWidth,
134+
left: (isRTL(this.el) ? -1 : 1) * offset,
131135
behavior: smoothScroll ? 'smooth' : 'instant',
132136
});
133137
}

core/src/components/segment-view/test/basic/segment-view.e2e.ts

Lines changed: 72 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { expect } from '@playwright/test';
22
import { configs, test } from '@utils/test/playwright';
33

44
/**
5-
* This behavior does not vary across modes/directions
5+
* This behavior does not vary across modes
66
*/
7-
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
7+
configs({ modes: ['md'] }).forEach(({ title, config }) => {
88
test.describe(title('segment-view: basic'), () => {
99
test('should show the first content with no initial value', async ({ page }) => {
1010
await page.setContent(
@@ -88,86 +88,86 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
8888
const segmentContent = page.locator('ion-segment-content[id="top"]');
8989
await expect(segmentContent).toBeInViewport();
9090
});
91-
});
9291

93-
test('should set correct segment button as checked when changing the value by scrolling the segment content', async ({
94-
page,
95-
}) => {
96-
await page.setContent(
97-
`
98-
<ion-segment value="free">
99-
<ion-segment-button content-id="paid" value="paid">
100-
<ion-label>Paid</ion-label>
101-
</ion-segment-button>
102-
<ion-segment-button content-id="free" value="free">
103-
<ion-label>Free</ion-label>
104-
</ion-segment-button>
105-
<ion-segment-button content-id="top" value="top">
106-
<ion-label>Top</ion-label>
107-
</ion-segment-button>
108-
</ion-segment>
109-
<ion-segment-view>
110-
<ion-segment-content id="paid">Paid</ion-segment-content>
111-
<ion-segment-content id="free">Free</ion-segment-content>
112-
<ion-segment-content id="top">Top</ion-segment-content>
113-
</ion-segment-view>
114-
`,
115-
config
116-
);
117-
118-
await page
119-
.locator('ion-segment-view')
120-
.evaluate(
121-
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
92+
test('should set correct segment button as checked when changing the value by scrolling the segment content', async ({
93+
page,
94+
}) => {
95+
await page.setContent(
96+
`
97+
<ion-segment value="free">
98+
<ion-segment-button content-id="paid" value="paid">
99+
<ion-label>Paid</ion-label>
100+
</ion-segment-button>
101+
<ion-segment-button content-id="free" value="free">
102+
<ion-label>Free</ion-label>
103+
</ion-segment-button>
104+
<ion-segment-button content-id="top" value="top">
105+
<ion-label>Top</ion-label>
106+
</ion-segment-button>
107+
</ion-segment>
108+
<ion-segment-view>
109+
<ion-segment-content id="paid">Paid</ion-segment-content>
110+
<ion-segment-content id="free">Free</ion-segment-content>
111+
<ion-segment-content id="top">Top</ion-segment-content>
112+
</ion-segment-view>
113+
`,
114+
config
122115
);
123116

124-
await page.waitForChanges();
117+
await page
118+
.locator('ion-segment-view')
119+
.evaluate(
120+
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
121+
);
125122

126-
await page.locator('ion-segment-content[id="top"]').scrollIntoViewIfNeeded();
123+
await page.waitForChanges();
127124

128-
const segmentButton = page.locator('ion-segment-button[value="top"]');
129-
await expect(segmentButton).toHaveClass(/segment-button-checked/);
130-
});
125+
await page.locator('ion-segment-content[id="top"]').scrollIntoViewIfNeeded();
126+
127+
const segmentButton = page.locator('ion-segment-button[value="top"]');
128+
await expect(segmentButton).toHaveClass(/segment-button-checked/);
129+
});
131130

132-
test('should set correct segment button as checked and show correct content when programmatically setting the segment vale', async ({
133-
page,
134-
}) => {
135-
await page.setContent(
136-
`
137-
<ion-segment value="free">
138-
<ion-segment-button content-id="paid" value="paid">
139-
<ion-label>Paid</ion-label>
140-
</ion-segment-button>
141-
<ion-segment-button content-id="free" value="free">
142-
<ion-label>Free</ion-label>
143-
</ion-segment-button>
144-
<ion-segment-button content-id="top" value="top">
145-
<ion-label>Top</ion-label>
146-
</ion-segment-button>
147-
</ion-segment>
148-
<ion-segment-view>
149-
<ion-segment-content id="paid">Paid</ion-segment-content>
150-
<ion-segment-content id="free">Free</ion-segment-content>
151-
<ion-segment-content id="top">Top</ion-segment-content>
152-
</ion-segment-view>
153-
`,
154-
config
155-
);
156-
157-
await page
158-
.locator('ion-segment-view')
159-
.evaluate(
160-
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
131+
test('should set correct segment button as checked and show correct content when programmatically setting the segment value', async ({
132+
page,
133+
}) => {
134+
await page.setContent(
135+
`
136+
<ion-segment value="free">
137+
<ion-segment-button content-id="paid" value="paid">
138+
<ion-label>Paid</ion-label>
139+
</ion-segment-button>
140+
<ion-segment-button content-id="free" value="free">
141+
<ion-label>Free</ion-label>
142+
</ion-segment-button>
143+
<ion-segment-button content-id="top" value="top">
144+
<ion-label>Top</ion-label>
145+
</ion-segment-button>
146+
</ion-segment>
147+
<ion-segment-view>
148+
<ion-segment-content id="paid">Paid</ion-segment-content>
149+
<ion-segment-content id="free">Free</ion-segment-content>
150+
<ion-segment-content id="top">Top</ion-segment-content>
151+
</ion-segment-view>
152+
`,
153+
config
161154
);
162155

163-
await page.waitForChanges();
156+
await page
157+
.locator('ion-segment-view')
158+
.evaluate(
159+
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
160+
);
161+
162+
await page.waitForChanges();
164163

165-
await page.locator('ion-segment').evaluate((segment: HTMLIonSegmentElement) => (segment.value = 'top'));
164+
await page.locator('ion-segment').evaluate((segment: HTMLIonSegmentElement) => (segment.value = 'top'));
166165

167-
const segmentButton = page.locator('ion-segment-button[value="top"]');
168-
await expect(segmentButton).toHaveClass(/segment-button-checked/);
166+
const segmentButton = page.locator('ion-segment-button[value="top"]');
167+
await expect(segmentButton).toHaveClass(/segment-button-checked/);
169168

170-
const segmentContent = page.locator('ion-segment-content[id="top"]');
171-
await expect(segmentContent).toBeInViewport();
169+
const segmentContent = page.locator('ion-segment-content[id="top"]');
170+
await expect(segmentContent).toBeInViewport();
171+
});
172172
});
173173
});
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<!DOCTYPE html>
2+
<html lang="en" dir="rtl">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>RTL Segment View - Basic</title>
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
9+
/>
10+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
11+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
12+
<script src="../../../../../scripts/testing/scripts.js"></script>
13+
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
14+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
15+
16+
<style>
17+
ion-segment-view {
18+
height: 100px;
19+
20+
margin-bottom: 20px;
21+
}
22+
23+
ion-segment-content {
24+
display: flex;
25+
justify-content: center;
26+
align-items: center;
27+
}
28+
29+
ion-segment-content:nth-of-type(3n + 1) {
30+
background: lightpink;
31+
}
32+
33+
ion-segment-content:nth-of-type(3n + 2) {
34+
background: lightblue;
35+
}
36+
37+
ion-segment-content:nth-of-type(3n + 3) {
38+
background: lightgreen;
39+
}
40+
</style>
41+
</head>
42+
43+
<body>
44+
<ion-app>
45+
<ion-header>
46+
<ion-toolbar>
47+
<ion-title>RTL Segment View - Basic</ion-title>
48+
</ion-toolbar>
49+
</ion-header>
50+
51+
<ion-content>
52+
<ion-segment id="noValueSegment">
53+
<ion-segment-button content-id="no" value="no">
54+
<ion-label>No</ion-label>
55+
</ion-segment-button>
56+
<ion-segment-button content-id="value" value="value">
57+
<ion-label>Value</ion-label>
58+
</ion-segment-button>
59+
</ion-segment>
60+
<ion-segment-view id="noValueSegmentView">
61+
<ion-segment-content id="no">No</ion-segment-content>
62+
<ion-segment-content id="value">Value</ion-segment-content>
63+
</ion-segment-view>
64+
65+
<ion-segment value="free">
66+
<ion-segment-button content-id="paid" value="paid">
67+
<ion-label>Paid</ion-label>
68+
</ion-segment-button>
69+
<ion-segment-button style="min-width: 200px" content-id="free" value="free">
70+
<ion-label>Free</ion-label>
71+
</ion-segment-button>
72+
<ion-segment-button content-id="top" value="top">
73+
<ion-label>Top</ion-label>
74+
</ion-segment-button>
75+
</ion-segment>
76+
<ion-segment-view>
77+
<ion-segment-content id="paid">Paid</ion-segment-content>
78+
<ion-segment-content id="free">Free</ion-segment-content>
79+
<ion-segment-content id="top">Top</ion-segment-content>
80+
</ion-segment-view>
81+
82+
<ion-segment value="peach" scrollable>
83+
<ion-segment-button content-id="orange" value="orange">
84+
<ion-label>Orange</ion-label>
85+
</ion-segment-button>
86+
<ion-segment-button content-id="banana" value="banana">
87+
<ion-label>Banana</ion-label>
88+
</ion-segment-button>
89+
<ion-segment-button content-id="pear" value="pear">
90+
<ion-label>Pear</ion-label>
91+
</ion-segment-button>
92+
<ion-segment-button content-id="peach" value="peach">
93+
<ion-label>Peach</ion-label>
94+
</ion-segment-button>
95+
<ion-segment-button content-id="grape" value="grape">
96+
<ion-label>Grape</ion-label>
97+
</ion-segment-button>
98+
<ion-segment-button content-id="mango" value="mango">
99+
<ion-label>Mango</ion-label>
100+
</ion-segment-button>
101+
<ion-segment-button content-id="apple" value="apple">
102+
<ion-label>Apple</ion-label>
103+
</ion-segment-button>
104+
<ion-segment-button content-id="strawberry" value="strawberry">
105+
<ion-label>Strawberry</ion-label>
106+
</ion-segment-button>
107+
<ion-segment-button content-id="cherry" value="cherry">
108+
<ion-label>Cherry</ion-label>
109+
</ion-segment-button>
110+
</ion-segment>
111+
<ion-segment-view>
112+
<ion-segment-content id="orange">Orange</ion-segment-content>
113+
<ion-segment-content id="banana">Banana</ion-segment-content>
114+
<ion-segment-content id="pear">Pear</ion-segment-content>
115+
<ion-segment-content id="peach">Peach</ion-segment-content>
116+
<ion-segment-content id="grape">Grape</ion-segment-content>
117+
<ion-segment-content id="mango">Mango</ion-segment-content>
118+
<ion-segment-content id="apple">Apple</ion-segment-content>
119+
<ion-segment-content id="strawberry">Strawberry</ion-segment-content>
120+
<ion-segment-content id="cherry">Cherry</ion-segment-content>
121+
</ion-segment-view>
122+
123+
<button class="expand" onClick="changeSegmentContent()">Change Segment Content</button>
124+
125+
<button class="expand" onClick="clearSegmentValue()">Clear Segment Value</button>
126+
127+
<button class="expand" onClick="addSegmentButtonAndContent()">Add New Segment Button & Content</button>
128+
</ion-content>
129+
130+
<ion-footer>
131+
<ion-toolbar>
132+
<ion-title>Footer</ion-title>
133+
</ion-toolbar>
134+
</ion-footer>
135+
136+
<script>
137+
function changeSegmentContent() {
138+
const segment = document.querySelector('#noValueSegment');
139+
const segmentView = document.querySelector('#noValueSegmentView');
140+
141+
let currentValue = segment.value;
142+
143+
if (currentValue === 'value') {
144+
currentValue = 'no';
145+
} else {
146+
currentValue = 'value';
147+
}
148+
149+
segment.value = currentValue;
150+
}
151+
152+
async function clearSegmentValue() {
153+
const segmentView = document.querySelector('#noValueSegmentView');
154+
segmentView.setContent('no', false);
155+
156+
// Set timeout to ensure the value is cleared after
157+
// the segment content is updated
158+
setTimeout(() => {
159+
const segment = document.querySelector('#noValueSegment');
160+
segment.value = undefined;
161+
});
162+
}
163+
164+
async function addSegmentButtonAndContent() {
165+
const segment = document.querySelector('ion-segment');
166+
const segmentView = document.querySelector('ion-segment-view');
167+
168+
const newButton = document.createElement('ion-segment-button');
169+
const newId = `new-${Date.now()}`;
170+
newButton.setAttribute('content-id', newId);
171+
newButton.setAttribute('value', newId);
172+
newButton.innerHTML = '<ion-label>New Button</ion-label>';
173+
174+
segment.appendChild(newButton);
175+
176+
setTimeout(() => {
177+
// Timeout to test waitForSegmentContent() in segment-button
178+
const newContent = document.createElement('ion-segment-content');
179+
newContent.setAttribute('id', newId);
180+
newContent.innerHTML = 'New Content';
181+
182+
segmentView.appendChild(newContent);
183+
184+
// Necessary timeout to ensure the value is set after the content is added.
185+
// Otherwise, the transition is unsuccessful and the content is not shown.
186+
setTimeout(() => {
187+
segment.setAttribute('value', newId);
188+
}, 200);
189+
}, 200);
190+
}
191+
</script>
192+
</ion-app>
193+
</body>
194+
</html>

0 commit comments

Comments
 (0)