fix(segment): scroll to active segment-button on first load (#28276)

Issue number: resolves #28096

---------

<!-- 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. -->

When a segment button is clicked, the segment will auto-scroll to
position the newly active button fully in view. However, this behavior
does not occur on first load. This means that when a segment is
initialized with a `value` corresponding to an off-screen button, the
button will remain off-screen.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

The same auto-scroll behavior from button click now also occurs on
component load.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
This commit is contained in:
Amanda Johnston
2023-10-04 12:11:32 -05:00
committed by GitHub
parent dc75392e9d
commit 1167a9325f
2 changed files with 84 additions and 26 deletions

View File

@ -1,6 +1,7 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core'; import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Prop, State, Watch, h, writeTask } from '@stencil/core'; import { Component, Element, Event, Host, Listen, Prop, State, Watch, h, writeTask } from '@stencil/core';
import type { Gesture, GestureDetail } from '@utils/gesture'; import type { Gesture, GestureDetail } from '@utils/gesture';
import { raf } from '@utils/helpers';
import { isRTL } from '@utils/rtl'; import { isRTL } from '@utils/rtl';
import { createColorClasses, hostContext } from '@utils/theme'; import { createColorClasses, hostContext } from '@utils/theme';
@ -83,31 +84,7 @@ export class Segment implements ComponentInterface {
* Used by `ion-segment-button` to determine if the button should be checked. * Used by `ion-segment-button` to determine if the button should be checked.
*/ */
this.ionSelect.emit({ value }); this.ionSelect.emit({ value });
this.scrollActiveButtonIntoView();
if (this.scrollable) {
const buttons = this.getButtons();
const activeButton = buttons.find((button) => button.value === value);
if (activeButton !== undefined) {
/**
* Scrollable segment buttons should be
* centered within the view including
* buttons that are partially offscreen.
*/
activeButton.scrollIntoView({
behavior: 'smooth',
inline: 'center',
/**
* Segment should scroll on the
* horizontal axis. `block: 'nearest'`
* ensures that the vertical axis
* does not scroll if the segment
* as a whole is already in view.
*/
block: 'nearest',
});
}
}
} }
/** /**
@ -163,6 +140,14 @@ export class Segment implements ComponentInterface {
async componentDidLoad() { async componentDidLoad() {
this.setCheckedClasses(); this.setCheckedClasses();
/**
* We need to wait for the buttons to all be rendered
* before we can scroll.
*/
raf(() => {
this.scrollActiveButtonIntoView();
});
this.gesture = (await import('../../utils/gesture')).createGesture({ this.gesture = (await import('../../utils/gesture')).createGesture({
el: this.el, el: this.el,
gestureName: 'segment', gestureName: 'segment',
@ -320,6 +305,35 @@ export class Segment implements ComponentInterface {
} }
} }
private scrollActiveButtonIntoView() {
const { scrollable, value } = this;
if (scrollable) {
const buttons = this.getButtons();
const activeButton = buttons.find((button) => button.value === value);
if (activeButton !== undefined) {
/**
* Scrollable segment buttons should be
* centered within the view including
* buttons that are partially offscreen.
*/
activeButton.scrollIntoView({
behavior: 'smooth',
inline: 'center',
/**
* Segment should scroll on the
* horizontal axis. `block: 'nearest'`
* ensures that the vertical axis
* does not scroll if the segment
* as a whole is already in view.
*/
block: 'nearest',
});
}
}
}
private setNextIndex(detail: GestureDetail, isEnd = false) { private setNextIndex(detail: GestureDetail, isEnd = false) {
const rtl = isRTL(this.el); const rtl = isRTL(this.el);
const activated = this.activated; const activated = this.activated;

View File

@ -2,7 +2,7 @@ import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright'; import { configs, test } from '@utils/test/playwright';
configs().forEach(({ title, screenshot, config }) => { configs().forEach(({ title, screenshot, config }) => {
test.describe(title('segment: scrollable'), () => { test.describe(title('segment: scrollable (rendering)'), () => {
test('should not have visual regressions', async ({ page }) => { test('should not have visual regressions', async ({ page }) => {
await page.setContent( await page.setContent(
` `
@ -45,3 +45,47 @@ configs().forEach(({ title, screenshot, config }) => {
}); });
}); });
}); });
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('segment: scrollable (functionality)'), () => {
test('should scroll active button into view when value is already set', async ({ page }) => {
await page.setContent(
`
<ion-segment scrollable="true" value="8">
<ion-segment-button value="1">
<ion-label>First</ion-label>
</ion-segment-button>
<ion-segment-button value="2">
<ion-label>Second</ion-label>
</ion-segment-button>
<ion-segment-button value="3">
<ion-label>Third</ion-label>
</ion-segment-button>
<ion-segment-button value="4">
<ion-label>Fourth</ion-label>
</ion-segment-button>
<ion-segment-button value="5">
<ion-label>Fifth</ion-label>
</ion-segment-button>
<ion-segment-button value="6">
<ion-label>Sixth</ion-label>
</ion-segment-button>
<ion-segment-button value="7">
<ion-label>Seventh</ion-label>
</ion-segment-button>
<ion-segment-button id="activeButton" value="8">
<ion-label>Eighth</ion-label>
</ion-segment-button>
<ion-segment-button value="9">
<ion-label>Ninth</ion-label>
</ion-segment-button>
</ion-segment>
`,
config
);
const activeButton = page.locator('#activeButton');
await expect(activeButton).toBeInViewport();
});
});
});