Skip to content

Commit 89508fb

Browse files
tanner-reitsbrandyscarney
authored and
Tanner Reits
committed
feat(segment-view): adds support for new ion-segment-view component (#29969)
Issue number: resolves internal --------- <!-- 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 can only be changed by clicking a segment button, or dragging the indicator ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> The segment/segment buttons can now be linked to segment content within a segment view component. This content is scrollable/swipeable. Changing the content will update the segment/indicator and vice-versa. ## 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. --> **Limitations:** - Segment buttons **cannot** be disabled when connected ton `ion-segment-content` instances - The `ion-segment` **cannot** be without a value when linked with an `ion-segment-view`. If no value is provided, the value will default to the value of the first `ion-segment-content` [Preview](https://ionic-framework-jlt8by2io-ionic1.vercel.app/src/components/segment-view/test/basic) [Preview (disabled state)](https://ionic-framework-jlt8by2io-ionic1.vercel.app/src/components/segment-view/test/disabled) --------- Co-authored-by: Brandy Carney <[email protected]>
1 parent 3628ea8 commit 89508fb

26 files changed

+1096
-23
lines changed

core/api.txt

+7
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,7 @@ ion-segment,css-prop,--background,ios
15421542
ion-segment,css-prop,--background,md
15431543

15441544
ion-segment-button,shadow
1545+
ion-segment-button,prop,contentId,string | undefined,undefined,false,true
15451546
ion-segment-button,prop,disabled,boolean,false,false,false
15461547
ion-segment-button,prop,layout,"icon-bottom" | "icon-end" | "icon-hide" | "icon-start" | "icon-top" | "label-hide" | undefined,'icon-top',false,false
15471548
ion-segment-button,prop,mode,"ios" | "md",undefined,false,false
@@ -1607,6 +1608,12 @@ ion-segment-button,part,indicator
16071608
ion-segment-button,part,indicator-background
16081609
ion-segment-button,part,native
16091610

1611+
ion-segment-content,shadow
1612+
1613+
ion-segment-view,shadow
1614+
ion-segment-view,prop,disabled,boolean,false,false,false
1615+
ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true
1616+
16101617
ion-select,shadow
16111618
ion-select,prop,cancelText,string,'Cancel',false,false
16121619
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true

core/src/components.d.ts

+68
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { NavigationHookCallback } from "./components/route/route-interface";
3434
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
3535
import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
3636
import { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
37+
import { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
3738
import { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
3839
import { SelectModalOption } from "./components/select-modal/select-modal-interface";
3940
import { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
@@ -70,6 +71,7 @@ export { NavigationHookCallback } from "./components/route/route-interface";
7071
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
7172
export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
7273
export { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
74+
export { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
7375
export { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
7476
export { SelectModalOption } from "./components/select-modal/select-modal-interface";
7577
export { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
@@ -2696,6 +2698,10 @@ export namespace Components {
26962698
"value"?: SegmentValue;
26972699
}
26982700
interface IonSegmentButton {
2701+
/**
2702+
* The `id` of the segment content.
2703+
*/
2704+
"contentId"?: string;
26992705
/**
27002706
* If `true`, the user cannot interact with the segment button.
27012707
*/
@@ -2718,6 +2724,19 @@ export namespace Components {
27182724
*/
27192725
"value": SegmentValue;
27202726
}
2727+
interface IonSegmentContent {
2728+
}
2729+
interface IonSegmentView {
2730+
/**
2731+
* If `true`, the segment view cannot be interacted with.
2732+
*/
2733+
"disabled": boolean;
2734+
/**
2735+
* @param id : The id of the segment content to display.
2736+
* @param smoothScroll : Whether to animate the scroll transition.
2737+
*/
2738+
"setContent": (id: string, smoothScroll?: boolean) => Promise<void>;
2739+
}
27212740
interface IonSelect {
27222741
/**
27232742
* The text to display on the cancel button.
@@ -3424,6 +3443,10 @@ export interface IonSegmentCustomEvent<T> extends CustomEvent<T> {
34243443
detail: T;
34253444
target: HTMLIonSegmentElement;
34263445
}
3446+
export interface IonSegmentViewCustomEvent<T> extends CustomEvent<T> {
3447+
detail: T;
3448+
target: HTMLIonSegmentViewElement;
3449+
}
34273450
export interface IonSelectCustomEvent<T> extends CustomEvent<T> {
34283451
detail: T;
34293452
target: HTMLIonSelectElement;
@@ -4420,6 +4443,29 @@ declare global {
44204443
prototype: HTMLIonSegmentButtonElement;
44214444
new (): HTMLIonSegmentButtonElement;
44224445
};
4446+
interface HTMLIonSegmentContentElement extends Components.IonSegmentContent, HTMLStencilElement {
4447+
}
4448+
var HTMLIonSegmentContentElement: {
4449+
prototype: HTMLIonSegmentContentElement;
4450+
new (): HTMLIonSegmentContentElement;
4451+
};
4452+
interface HTMLIonSegmentViewElementEventMap {
4453+
"ionSegmentViewScroll": SegmentViewScrollEvent;
4454+
}
4455+
interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement {
4456+
addEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
4457+
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
4458+
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
4459+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
4460+
removeEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
4461+
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
4462+
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
4463+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
4464+
}
4465+
var HTMLIonSegmentViewElement: {
4466+
prototype: HTMLIonSegmentViewElement;
4467+
new (): HTMLIonSegmentViewElement;
4468+
};
44234469
interface HTMLIonSelectElementEventMap {
44244470
"ionChange": SelectChangeEventDetail;
44254471
"ionCancel": void;
@@ -4735,6 +4781,8 @@ declare global {
47354781
"ion-searchbar": HTMLIonSearchbarElement;
47364782
"ion-segment": HTMLIonSegmentElement;
47374783
"ion-segment-button": HTMLIonSegmentButtonElement;
4784+
"ion-segment-content": HTMLIonSegmentContentElement;
4785+
"ion-segment-view": HTMLIonSegmentViewElement;
47384786
"ion-select": HTMLIonSelectElement;
47394787
"ion-select-modal": HTMLIonSelectModalElement;
47404788
"ion-select-option": HTMLIonSelectOptionElement;
@@ -7465,6 +7513,10 @@ declare namespace LocalJSX {
74657513
"value"?: SegmentValue;
74667514
}
74677515
interface IonSegmentButton {
7516+
/**
7517+
* The `id` of the segment content.
7518+
*/
7519+
"contentId"?: string;
74687520
/**
74697521
* If `true`, the user cannot interact with the segment button.
74707522
*/
@@ -7486,6 +7538,18 @@ declare namespace LocalJSX {
74867538
*/
74877539
"value"?: SegmentValue;
74887540
}
7541+
interface IonSegmentContent {
7542+
}
7543+
interface IonSegmentView {
7544+
/**
7545+
* If `true`, the segment view cannot be interacted with.
7546+
*/
7547+
"disabled"?: boolean;
7548+
/**
7549+
* Emitted when the segment view is scrolled.
7550+
*/
7551+
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<SegmentViewScrollEvent>) => void;
7552+
}
74897553
interface IonSelect {
74907554
/**
74917555
* The text to display on the cancel button.
@@ -8182,6 +8246,8 @@ declare namespace LocalJSX {
81828246
"ion-searchbar": IonSearchbar;
81838247
"ion-segment": IonSegment;
81848248
"ion-segment-button": IonSegmentButton;
8249+
"ion-segment-content": IonSegmentContent;
8250+
"ion-segment-view": IonSegmentView;
81858251
"ion-select": IonSelect;
81868252
"ion-select-modal": IonSelectModal;
81878253
"ion-select-option": IonSelectOption;
@@ -8282,6 +8348,8 @@ declare module "@stencil/core" {
82828348
"ion-searchbar": LocalJSX.IonSearchbar & JSXBase.HTMLAttributes<HTMLIonSearchbarElement>;
82838349
"ion-segment": LocalJSX.IonSegment & JSXBase.HTMLAttributes<HTMLIonSegmentElement>;
82848350
"ion-segment-button": LocalJSX.IonSegmentButton & JSXBase.HTMLAttributes<HTMLIonSegmentButtonElement>;
8351+
"ion-segment-content": LocalJSX.IonSegmentContent & JSXBase.HTMLAttributes<HTMLIonSegmentContentElement>;
8352+
"ion-segment-view": LocalJSX.IonSegmentView & JSXBase.HTMLAttributes<HTMLIonSegmentViewElement>;
82858353
"ion-select": LocalJSX.IonSelect & JSXBase.HTMLAttributes<HTMLIonSelectElement>;
82868354
"ion-select-modal": LocalJSX.IonSelectModal & JSXBase.HTMLAttributes<HTMLIonSelectModalElement>;
82878355
"ion-select-option": LocalJSX.IonSelectOption & JSXBase.HTMLAttributes<HTMLIonSelectOptionElement>;

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

+30-7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
3636

3737
@State() checked = false;
3838

39+
/**
40+
* The `id` of the segment content.
41+
*/
42+
@Prop({ reflect: true }) contentId?: string;
43+
3944
/**
4045
* If `true`, the user cannot interact with the segment button.
4146
*/
@@ -67,6 +72,30 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
6772
addEventListener(segmentEl, 'ionSelect', this.updateState);
6873
addEventListener(segmentEl, 'ionStyle', this.updateStyle);
6974
}
75+
76+
// Return if there is no contentId defined
77+
if (!this.contentId) return;
78+
79+
// Attempt to find the Segment Content by its contentId
80+
const segmentContent = document.getElementById(this.contentId) as HTMLIonSegmentContentElement | null;
81+
82+
// If no associated Segment Content exists, log an error and return
83+
if (!segmentContent) {
84+
console.error(`Segment Button: Unable to find Segment Content with id="${this.contentId}".`);
85+
return;
86+
}
87+
88+
// Ensure the found element is a valid ION-SEGMENT-CONTENT
89+
if (segmentContent.tagName !== 'ION-SEGMENT-CONTENT') {
90+
console.error(`Segment Button: Element with id="${this.contentId}" is not an <ion-segment-content> element.`);
91+
return;
92+
}
93+
94+
// Prevent buttons from being disabled when associated with segment content
95+
if (this.disabled) {
96+
console.warn(`Segment Button: Segment buttons cannot be disabled when associated with an <ion-segment-content>.`);
97+
this.disabled = false;
98+
}
7099
}
71100

72101
disconnectedCallback() {
@@ -161,13 +190,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
161190
</span>
162191
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
163192
</button>
164-
<div
165-
part="indicator"
166-
class={{
167-
'segment-button-indicator': true,
168-
'segment-button-indicator-animated': true,
169-
}}
170-
>
193+
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
171194
<div part="indicator-background" class="segment-button-indicator-background"></div>
172195
</div>
173196
</Host>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Segment Content
2+
// --------------------------------------------------
3+
4+
:host {
5+
scroll-snap-align: center;
6+
scroll-snap-stop: always;
7+
8+
flex-shrink: 0;
9+
10+
width: 100%;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { ComponentInterface } from '@stencil/core';
2+
import { Component, Host, h } from '@stencil/core';
3+
4+
@Component({
5+
tag: 'ion-segment-content',
6+
styleUrl: 'segment-content.scss',
7+
shadow: true,
8+
})
9+
export class SegmentContent implements ComponentInterface {
10+
render() {
11+
return (
12+
<Host>
13+
<slot></slot>
14+
</Host>
15+
);
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface SegmentViewScrollEvent {
2+
scrollRatio: number;
3+
isManualScroll: boolean;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@import "./segment-view";
2+
@import "../segment-button/segment-button.ios.vars";
3+
4+
// iOS Segment View
5+
// --------------------------------------------------
6+
7+
:host(.segment-view-disabled) {
8+
opacity: $segment-button-ios-opacity-disabled;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@import "./segment-view";
2+
@import "../segment-button/segment-button.md.vars";
3+
4+
// Material Design Segment View
5+
// --------------------------------------------------
6+
7+
:host(.segment-view-disabled) {
8+
opacity: $segment-button-md-opacity-disabled;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Segment View
2+
// --------------------------------------------------
3+
4+
:host {
5+
display: flex;
6+
7+
height: 100%;
8+
9+
overflow-x: scroll;
10+
scroll-snap-type: x mandatory;
11+
12+
/* Hide scrollbar in Firefox */
13+
scrollbar-width: none;
14+
15+
/* Hide scrollbar in IE and Edge */
16+
-ms-overflow-style: none;
17+
}
18+
19+
/* Hide scrollbar in webkit */
20+
:host::-webkit-scrollbar {
21+
display: none;
22+
}
23+
24+
:host(.segment-view-disabled) {
25+
touch-action: none;
26+
overflow-x: hidden;
27+
}
28+
29+
:host(.segment-view-scroll-disabled) {
30+
pointer-events: none;
31+
}

0 commit comments

Comments
 (0)