Compare commits

...

7 Commits

Author SHA1 Message Date
Brandy Carney
82bda3e20d say no to memory leaks man cook kiss 2024-07-15 18:09:24 -04:00
Brandy Carney
a331afe253 style: type cleanup 2024-07-15 17:26:46 -04:00
Brandy Carney
6bf510d71e style: code cleanup 2024-07-15 15:29:44 -04:00
Brandy Carney
9e3ddfd50d style: lint 2024-07-15 13:23:52 -04:00
Brandy Carney
af405ccfc6 test(segment): show examples using divs and new components 2024-07-15 13:22:29 -04:00
Brandy Carney
e3aeae97a4 feat(segment): add segment view and content components 2024-07-15 13:22:20 -04:00
Brandy Carney
f03d423a5f fix(segment): animate highlight when value changes 2024-07-12 14:41:36 -04:00
13 changed files with 495 additions and 2 deletions

View File

@@ -3172,6 +3172,10 @@ export namespace Components {
*/
"value": SegmentValue;
}
interface IonSegmentContent {
}
interface IonSegmentView {
}
interface IonSelect {
/**
* The text to display on the cancel button.
@@ -4973,6 +4977,18 @@ declare global {
prototype: HTMLIonSegmentButtonElement;
new (): HTMLIonSegmentButtonElement;
};
interface HTMLIonSegmentContentElement extends Components.IonSegmentContent, HTMLStencilElement {
}
var HTMLIonSegmentContentElement: {
prototype: HTMLIonSegmentContentElement;
new (): HTMLIonSegmentContentElement;
};
interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement {
}
var HTMLIonSegmentViewElement: {
prototype: HTMLIonSegmentViewElement;
new (): HTMLIonSegmentViewElement;
};
interface HTMLIonSelectElementEventMap {
"ionChange": SelectChangeEventDetail;
"ionCancel": void;
@@ -5282,6 +5298,8 @@ declare global {
"ion-searchbar": HTMLIonSearchbarElement;
"ion-segment": HTMLIonSegmentElement;
"ion-segment-button": HTMLIonSegmentButtonElement;
"ion-segment-content": HTMLIonSegmentContentElement;
"ion-segment-view": HTMLIonSegmentViewElement;
"ion-select": HTMLIonSelectElement;
"ion-select-option": HTMLIonSelectOptionElement;
"ion-select-popover": HTMLIonSelectPopoverElement;
@@ -8492,6 +8510,10 @@ declare namespace LocalJSX {
*/
"value"?: SegmentValue;
}
interface IonSegmentContent {
}
interface IonSegmentView {
}
interface IonSelect {
/**
* The text to display on the cancel button.
@@ -9287,6 +9309,8 @@ declare namespace LocalJSX {
"ion-searchbar": IonSearchbar;
"ion-segment": IonSegment;
"ion-segment-button": IonSegmentButton;
"ion-segment-content": IonSegmentContent;
"ion-segment-view": IonSegmentView;
"ion-select": IonSelect;
"ion-select-option": IonSelectOption;
"ion-select-popover": IonSelectPopover;
@@ -9386,6 +9410,8 @@ declare module "@stencil/core" {
"ion-searchbar": LocalJSX.IonSearchbar & JSXBase.HTMLAttributes<HTMLIonSearchbarElement>;
"ion-segment": LocalJSX.IonSegment & JSXBase.HTMLAttributes<HTMLIonSegmentElement>;
"ion-segment-button": LocalJSX.IonSegmentButton & JSXBase.HTMLAttributes<HTMLIonSegmentButtonElement>;
"ion-segment-content": LocalJSX.IonSegmentContent & JSXBase.HTMLAttributes<HTMLIonSegmentContentElement>;
"ion-segment-view": LocalJSX.IonSegmentView & JSXBase.HTMLAttributes<HTMLIonSegmentViewElement>;
"ion-select": LocalJSX.IonSelect & JSXBase.HTMLAttributes<HTMLIonSelectElement>;
"ion-select-option": LocalJSX.IonSelectOption & JSXBase.HTMLAttributes<HTMLIonSelectOptionElement>;
"ion-select-popover": LocalJSX.IonSelectPopover & JSXBase.HTMLAttributes<HTMLIonSelectPopoverElement>;

View File

@@ -0,0 +1,10 @@
// Segment Content
// --------------------------------------------------
:host {
scroll-snap-align: center;
flex-shrink: 0;
width: 100%;
}

View File

@@ -0,0 +1,17 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Host, h } from '@stencil/core';
@Component({
tag: 'ion-segment-content',
styleUrl: 'segment-content.scss',
shadow: true,
})
export class SegmentContent implements ComponentInterface {
render() {
return (
<Host>
<slot></slot>
</Host>
);
}
}

View File

@@ -0,0 +1,20 @@
// Segment View
// --------------------------------------------------
:host {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
/* Hide scrollbar in Firefox */
scrollbar-width: none;
/* Hide scrollbar in IE and Edge */
-ms-overflow-style: none;
}
/* Hide scrollbar in webkit */
:host::-webkit-scrollbar {
display: none;
}

View File

@@ -0,0 +1,90 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Listen, h } from '@stencil/core';
import { addEventListener, removeEventListener } from '@utils/helpers';
@Component({
tag: 'ion-segment-view',
styleUrl: 'segment-view.scss',
shadow: true,
})
export class SegmentView implements ComponentInterface {
private segmentEl: HTMLIonSegmentElement | null = null;
@Element() el!: HTMLElement;
@Listen('scroll')
segmentViewScroll(ev: any) {
const { segmentEl } = this;
const atSnappingPoint = ev.target.scrollLeft % ev.target.offsetWidth === 0;
if (atSnappingPoint) {
const index = Math.round(ev.target.scrollLeft / ev.target.offsetWidth);
const segmentButton = this.getSegmentButtonAtIndex(index);
if (segmentEl) {
segmentEl.value = segmentButton.value;
}
}
}
connectedCallback() {
const segmentEl = (this.segmentEl = document.querySelector(`ion-segment[view=${this.el.id}]`));
if (segmentEl) {
addEventListener(segmentEl, 'ionChange', this.updateSection);
}
}
disconnectedCallback() {
const segmentEl = this.segmentEl;
if (segmentEl) {
removeEventListener(segmentEl, 'ionChange', this.updateSection);
this.segmentEl = null;
}
}
private updateSection = () => {
const { segmentEl } = this;
if (segmentEl) {
const value = segmentEl.value;
const index = this.getSegmentButtonIndexWithValue(value);
this.setSection(index);
}
};
private setSection = (index: number) => {
const sectionWidth = this.el.offsetWidth;
this.el.scrollTo({
top: 0,
left: index * sectionWidth,
behavior: 'smooth',
});
};
private getSegmentButtons(): HTMLIonSegmentButtonElement[] {
const { segmentEl } = this;
if (!segmentEl) {
return [];
}
return Array.from(segmentEl.querySelectorAll('ion-segment-button'));
}
private getSegmentButtonAtIndex(index: number) {
return this.getSegmentButtons()[index];
}
private getSegmentButtonIndexWithValue(value: any) {
return this.getSegmentButtons().findIndex((b) => b.value === value);
}
render() {
return (
<Host>
<slot></slot>
</Host>
);
}
}

View File

@@ -80,7 +80,17 @@ export class Segment implements ComponentInterface {
@Prop({ mutable: true }) value?: SegmentValue;
@Watch('value')
protected valueChanged(value: SegmentValue | undefined) {
protected valueChanged(value: SegmentValue | undefined, oldValue?: SegmentValue | undefined) {
if (oldValue !== undefined && value !== undefined) {
const buttons = this.getButtons();
const previous = buttons.find((button) => button.value === oldValue);
const current = buttons.find((button) => button.value === value);
if (previous && current) {
this.checkButton(previous, current);
}
}
/**
* `ionSelect` is emitted every time the value changes (internal or external changes).
* Used by `ion-segment-button` to determine if the button should be checked.
@@ -210,7 +220,7 @@ export class Segment implements ComponentInterface {
this.ionChange.emit({ value });
}
private getButtons() {
private getButtons(): HTMLIonSegmentButtonElement[] {
return Array.from(this.el.querySelectorAll('ion-segment-button'));
}

View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Segment - Content Component</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Segment - Content Component</ion-title>
</ion-toolbar>
<ion-toolbar>
<ion-segment view="myView" value="home">
<ion-segment-button value="home">
<ion-icon name="triangle-outline"></ion-icon>
<ion-label>Home</ion-label>
</ion-segment-button>
<ion-segment-button value="radio">
<ion-icon name="triangle-outline"></ion-icon>
<ion-label>Radio</ion-label>
</ion-segment-button>
<ion-segment-button value="library">
<ion-icon name="triangle-outline"></ion-icon>
<ion-label>Library</ion-label>
</ion-segment-button>
<ion-segment-button value="search">
<ion-icon name="triangle-outline"></ion-icon>
<ion-label>Search</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-segment-view id="myView">
<ion-segment-content>
<ion-card>
<ion-card-header>
<ion-card-title>Home card title</ion-card-title>
<ion-card-content>Home card content</ion-card-content>
</ion-card-header>
</ion-card>
</ion-segment-content>
<ion-segment-content>
<ion-card>
<ion-card-header>
<ion-card-title>Radio card title</ion-card-title>
<ion-card-content>Radio card content</ion-card-content>
</ion-card-header>
</ion-card>
</ion-segment-content>
<ion-segment-content>
<ion-card>
<ion-card-header>
<ion-card-title>Library card title</ion-card-title>
<ion-card-content>Library card content</ion-card-content>
</ion-card-header>
</ion-card>
</ion-segment-content>
<ion-segment-content>
<ion-card>
<ion-card-header>
<ion-card-title>Search card title</ion-card-title>
<ion-card-content>Search card content</ion-card-content>
</ion-card-header>
</ion-card>
</ion-segment-content>
</ion-segment-view>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Segment - Content</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
#segment-view {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
/* Hide scrollbar in Firefox */
scrollbar-width: none;
/* Hide scrollbar in IE and Edge */
-ms-overflow-style: none;
}
/* Hide scrollbar in webkit */
#segment-view::-webkit-scrollbar {
display: none;
}
#segment-view .segment-content {
scroll-snap-align: center;
flex-shrink: 0;
width: 100%;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Segment - Content</ion-title>
</ion-toolbar>
<ion-toolbar>
<ion-segment value="0">
<ion-segment-button value="0">
<ion-icon name="triangle-outline"></ion-icon>
<ion-label>Home</ion-label>
</ion-segment-button>
<ion-segment-button value="1">
<ion-icon name="triangle-outline"></ion-icon>
<ion-label>Radio</ion-label>
</ion-segment-button>
<ion-segment-button value="2">
<ion-icon name="triangle-outline"></ion-icon>
<ion-label>Library</ion-label>
</ion-segment-button>
<ion-segment-button value="3">
<ion-icon name="triangle-outline"></ion-icon>
<ion-label>Search</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
<ion-content>
<div id="segment-view">
<div id="home" class="segment-content">
<ion-card>
<ion-card-header>
<ion-card-title>Home card title</ion-card-title>
<ion-card-content>Home card content</ion-card-content>
</ion-card-header>
</ion-card>
</div>
<div id="radio" class="segment-content">
<ion-card>
<ion-card-header>
<ion-card-title>Radio card title</ion-card-title>
<ion-card-content>Radio card content</ion-card-content>
</ion-card-header>
</ion-card>
</div>
<div id="library" class="segment-content">
<ion-card>
<ion-card-header>
<ion-card-title>Library card title</ion-card-title>
<ion-card-content>Library card content</ion-card-content>
</ion-card-header>
</ion-card>
</div>
<div id="search" class="segment-content">
<ion-card>
<ion-card-header>
<ion-card-title>Search card title</ion-card-title>
<ion-card-content>Search card content</ion-card-content>
</ion-card-header>
</ion-card>
</div>
</div>
</ion-content>
</ion-app>
<script>
const segment = document.querySelector('ion-segment');
const segmentView = document.querySelector('#segment-view');
segment.addEventListener('ionChange', function (event) {
const index = event.detail.value;
const sectionWidth = segmentView.offsetWidth;
segmentView.scrollTo({
top: 0,
left: index * sectionWidth,
behavior: 'smooth',
});
});
segmentView.addEventListener('scroll', function (event) {
// Calculate the index of the current section when the scrolling ends
var atSnappingPoint = event.target.scrollLeft % event.target.offsetWidth === 0;
if (atSnappingPoint) {
const index = Math.round(event.target.scrollLeft / event.target.offsetWidth);
segment.value = `${index}`;
}
});
</script>
</body>
</html>

View File

@@ -69,6 +69,8 @@ export const DIRECTIVES = [
d.IonSearchbar,
d.IonSegment,
d.IonSegmentButton,
d.IonSegmentContent,
d.IonSegmentView,
d.IonSelect,
d.IonSelectOption,
d.IonSkeletonText,

View File

@@ -2011,6 +2011,48 @@ export class IonSegmentButton {
export declare interface IonSegmentButton extends Components.IonSegmentButton {}
@ProxyCmp({
})
@Component({
selector: 'ion-segment-content',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: [],
})
export class IonSegmentContent {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}
export declare interface IonSegmentContent extends Components.IonSegmentContent {}
@ProxyCmp({
})
@Component({
selector: 'ion-segment-view',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: [],
})
export class IonSegmentView {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}
export declare interface IonSegmentView extends Components.IonSegmentView {}
@ProxyCmp({
inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'theme', 'toggleIcon', 'value'],
methods: ['open']

View File

@@ -65,6 +65,8 @@ import { defineCustomElement as defineIonReorderGroup } from '@ionic/core/compon
import { defineCustomElement as defineIonRippleEffect } from '@ionic/core/components/ion-ripple-effect.js';
import { defineCustomElement as defineIonRow } from '@ionic/core/components/ion-row.js';
import { defineCustomElement as defineIonSegmentButton } from '@ionic/core/components/ion-segment-button.js';
import { defineCustomElement as defineIonSegmentContent } from '@ionic/core/components/ion-segment-content.js';
import { defineCustomElement as defineIonSegmentView } from '@ionic/core/components/ion-segment-view.js';
import { defineCustomElement as defineIonSelectOption } from '@ionic/core/components/ion-select-option.js';
import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js';
import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js';
@@ -1842,6 +1844,52 @@ export class IonSegmentButton {
export declare interface IonSegmentButton extends Components.IonSegmentButton {}
@ProxyCmp({
defineCustomElementFn: defineIonSegmentContent
})
@Component({
selector: 'ion-segment-content',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: [],
standalone: true
})
export class IonSegmentContent {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}
export declare interface IonSegmentContent extends Components.IonSegmentContent {}
@ProxyCmp({
defineCustomElementFn: defineIonSegmentView
})
@Component({
selector: 'ion-segment-view',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: [],
standalone: true
})
export class IonSegmentView {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}
export declare interface IonSegmentView extends Components.IonSegmentView {}
@ProxyCmp({
defineCustomElementFn: defineIonSelectOption,
inputs: ['disabled', 'mode', 'theme', 'value']

View File

@@ -61,6 +61,8 @@ import { defineCustomElement as defineIonRow } from '@ionic/core/components/ion-
import { defineCustomElement as defineIonSearchbar } from '@ionic/core/components/ion-searchbar.js';
import { defineCustomElement as defineIonSegment } from '@ionic/core/components/ion-segment.js';
import { defineCustomElement as defineIonSegmentButton } from '@ionic/core/components/ion-segment-button.js';
import { defineCustomElement as defineIonSegmentContent } from '@ionic/core/components/ion-segment-content.js';
import { defineCustomElement as defineIonSegmentView } from '@ionic/core/components/ion-segment-view.js';
import { defineCustomElement as defineIonSelect } from '@ionic/core/components/ion-select.js';
import { defineCustomElement as defineIonSelectOption } from '@ionic/core/components/ion-select-option.js';
import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js';
@@ -130,6 +132,8 @@ export const IonRow = /*@__PURE__*/createReactComponent<JSX.IonRow, HTMLIonRowEl
export const IonSearchbar = /*@__PURE__*/createReactComponent<JSX.IonSearchbar, HTMLIonSearchbarElement>('ion-searchbar', undefined, undefined, defineIonSearchbar);
export const IonSegment = /*@__PURE__*/createReactComponent<JSX.IonSegment, HTMLIonSegmentElement>('ion-segment', undefined, undefined, defineIonSegment);
export const IonSegmentButton = /*@__PURE__*/createReactComponent<JSX.IonSegmentButton, HTMLIonSegmentButtonElement>('ion-segment-button', undefined, undefined, defineIonSegmentButton);
export const IonSegmentContent = /*@__PURE__*/createReactComponent<JSX.IonSegmentContent, HTMLIonSegmentContentElement>('ion-segment-content', undefined, undefined, defineIonSegmentContent);
export const IonSegmentView = /*@__PURE__*/createReactComponent<JSX.IonSegmentView, HTMLIonSegmentViewElement>('ion-segment-view', undefined, undefined, defineIonSegmentView);
export const IonSelect = /*@__PURE__*/createReactComponent<JSX.IonSelect, HTMLIonSelectElement>('ion-select', undefined, undefined, defineIonSelect);
export const IonSelectOption = /*@__PURE__*/createReactComponent<JSX.IonSelectOption, HTMLIonSelectOptionElement>('ion-select-option', undefined, undefined, defineIonSelectOption);
export const IonSkeletonText = /*@__PURE__*/createReactComponent<JSX.IonSkeletonText, HTMLIonSkeletonTextElement>('ion-skeleton-text', undefined, undefined, defineIonSkeletonText);

View File

@@ -67,6 +67,8 @@ import { defineCustomElement as defineIonRow } from '@ionic/core/components/ion-
import { defineCustomElement as defineIonSearchbar } from '@ionic/core/components/ion-searchbar.js';
import { defineCustomElement as defineIonSegment } from '@ionic/core/components/ion-segment.js';
import { defineCustomElement as defineIonSegmentButton } from '@ionic/core/components/ion-segment-button.js';
import { defineCustomElement as defineIonSegmentContent } from '@ionic/core/components/ion-segment-content.js';
import { defineCustomElement as defineIonSegmentView } from '@ionic/core/components/ion-segment-view.js';
import { defineCustomElement as defineIonSelect } from '@ionic/core/components/ion-select.js';
import { defineCustomElement as defineIonSelectOption } from '@ionic/core/components/ion-select-option.js';
import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js';
@@ -765,6 +767,12 @@ export const IonSegmentButton = /*@__PURE__*/ defineContainer<JSX.IonSegmentButt
'value', 'ion-change');
export const IonSegmentContent = /*@__PURE__*/ defineContainer<JSX.IonSegmentContent>('ion-segment-content', defineIonSegmentContent);
export const IonSegmentView = /*@__PURE__*/ defineContainer<JSX.IonSegmentView>('ion-segment-view', defineIonSegmentView);
export const IonSelect = /*@__PURE__*/ defineContainer<JSX.IonSelect, JSX.IonSelect["value"]>('ion-select', defineIonSelect, [
'cancelText',
'color',