Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@ ion-button,css-prop,--transition,ios
ion-button,css-prop,--transition,md
ion-button,part,native

ion-button-group,scoped
ion-button-group,prop,mode,"ios" | "md",undefined,false,false
ion-button-group,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-button-group,prop,value,null | number | string,null,false,false
ion-button-group,event,ionChange,{ value: string | number | null; activeIndex: number; },true

ion-buttons,scoped
ion-buttons,prop,collapse,boolean,false,false,false
ion-buttons,prop,mode,"ios" | "md",undefined,false,false
Expand Down
66 changes: 66 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,24 @@ export namespace Components {
*/
"type": 'submit' | 'reset' | 'button';
}
interface IonButtonGroup {
"color"?: Color;
"fill"?: 'outline' | 'solid';
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
"shape"?: 'soft' | 'round' | 'rectangular';
"size"?: 'small' | 'default' | 'large';
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* The value of the currently selected button.
*/
"value": string | number | null;
}
interface IonButtons {
/**
* If true, buttons will disappear when its parent toolbar has fully collapsed if the toolbar is not the first toolbar. If the toolbar is the first toolbar, the buttons will be hidden and will only be shown once all toolbars have fully collapsed. Only applies in the `ios` theme with `collapse` set to `true` on `ion-header`. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles)
Expand Down Expand Up @@ -4019,6 +4037,10 @@ export interface IonButtonCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonButtonElement;
}
export interface IonButtonGroupCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonButtonGroupElement;
}
export interface IonCheckboxCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonCheckboxElement;
Expand Down Expand Up @@ -4351,6 +4373,24 @@ declare global {
prototype: HTMLIonButtonElement;
new (): HTMLIonButtonElement;
};
interface HTMLIonButtonGroupElementEventMap {
"ionChange": { value: string | number | null; activeIndex: number };
"ionValueChange": { value: string | number | null};
}
interface HTMLIonButtonGroupElement extends Components.IonButtonGroup, HTMLStencilElement {
addEventListener<K extends keyof HTMLIonButtonGroupElementEventMap>(type: K, listener: (this: HTMLIonButtonGroupElement, ev: IonButtonGroupCustomEvent<HTMLIonButtonGroupElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLIonButtonGroupElementEventMap>(type: K, listener: (this: HTMLIonButtonGroupElement, ev: IonButtonGroupCustomEvent<HTMLIonButtonGroupElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLIonButtonGroupElement: {
prototype: HTMLIonButtonGroupElement;
new (): HTMLIonButtonGroupElement;
};
interface HTMLIonButtonsElement extends Components.IonButtons, HTMLStencilElement {
}
var HTMLIonButtonsElement: {
Expand Down Expand Up @@ -5419,6 +5459,7 @@ declare global {
"ion-breadcrumb": HTMLIonBreadcrumbElement;
"ion-breadcrumbs": HTMLIonBreadcrumbsElement;
"ion-button": HTMLIonButtonElement;
"ion-button-group": HTMLIonButtonGroupElement;
"ion-buttons": HTMLIonButtonsElement;
"ion-card": HTMLIonCardElement;
"ion-card-content": HTMLIonCardContentElement;
Expand Down Expand Up @@ -6088,6 +6129,29 @@ declare namespace LocalJSX {
*/
"type"?: 'submit' | 'reset' | 'button';
}
interface IonButtonGroup {
"color"?: Color;
"fill"?: 'outline' | 'solid';
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* Emitted when the active button changes.
*/
"onIonChange"?: (event: IonButtonGroupCustomEvent<{ value: string | number | null; activeIndex: number }>) => void;
"onIonValueChange"?: (event: IonButtonGroupCustomEvent<{ value: string | number | null}>) => void;
"shape"?: 'soft' | 'round' | 'rectangular';
"size"?: 'small' | 'default' | 'large';
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* The value of the currently selected button.
*/
"value"?: string | number | null;
}
interface IonButtons {
/**
* If true, buttons will disappear when its parent toolbar has fully collapsed if the toolbar is not the first toolbar. If the toolbar is the first toolbar, the buttons will be hidden and will only be shown once all toolbars have fully collapsed. Only applies in the `ios` theme with `collapse` set to `true` on `ion-header`. Typically used for [Collapsible Large Titles](https://ionicframework.com/docs/api/title#collapsible-large-titles)
Expand Down Expand Up @@ -9579,6 +9643,7 @@ declare namespace LocalJSX {
"ion-breadcrumb": IonBreadcrumb;
"ion-breadcrumbs": IonBreadcrumbs;
"ion-button": IonButton;
"ion-button-group": IonButtonGroup;
"ion-buttons": IonButtons;
"ion-card": IonCard;
"ion-card-content": IonCardContent;
Expand Down Expand Up @@ -9682,6 +9747,7 @@ declare module "@stencil/core" {
"ion-breadcrumb": LocalJSX.IonBreadcrumb & JSXBase.HTMLAttributes<HTMLIonBreadcrumbElement>;
"ion-breadcrumbs": LocalJSX.IonBreadcrumbs & JSXBase.HTMLAttributes<HTMLIonBreadcrumbsElement>;
"ion-button": LocalJSX.IonButton & JSXBase.HTMLAttributes<HTMLIonButtonElement>;
"ion-button-group": LocalJSX.IonButtonGroup & JSXBase.HTMLAttributes<HTMLIonButtonGroupElement>;
"ion-buttons": LocalJSX.IonButtons & JSXBase.HTMLAttributes<HTMLIonButtonsElement>;
"ion-card": LocalJSX.IonCard & JSXBase.HTMLAttributes<HTMLIonCardElement>;
"ion-card-content": LocalJSX.IonCardContent & JSXBase.HTMLAttributes<HTMLIonCardContentElement>;
Expand Down
103 changes: 103 additions & 0 deletions core/src/components/button-group/button-group.ionic.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
@use "../../themes/ionic/ionic.globals.scss" as globals;

:host {
--border-radius: #{globals.$ion-border-radius-full};
display: flex;
position: relative;

border-radius: var(--border-radius);

background-color: #f2f4fd;
}

.active-indicator {
position: absolute;
top: 0;
bottom: 0;
left: 0;

transition: transform 0.3s ease;

border-radius: var(--border-radius);

background-color: #105cef;

z-index: 0;
}

::slotted(ion-button) {
--color: #105cef;
--background-hover: transparent;
--background-activated: transparent;
flex: 1;

background-color: transparent;
transition: color 0.3s ease;
will-change: color;

z-index: 1;
}

::slotted(ion-button.active) {
color: #fff;

}

// ButtonGroup Shapes
// -------------------------------------------------------------------------------

// Soft Button
// --------------------------------------------------

:host(.button-group-soft) {
--border-radius: #{globals.$ion-border-radius-200};
}

// Round Button
// --------------------------------------------------

:host(.button-group-round) {
--border-radius: #{globals.$ion-border-radius-full};
}

// Rectangular Button
// --------------------------------------------------

:host(.button-group-rectangular) {
--border-radius: #{globals.$ion-border-radius-0};
}


// ButtonGroup Ouline
// -------------------------------------------------------------------------------

:host(.button-group-outline) .active-indicator {
border: 1px solid #105cef;

background-color: #fff;
}

:host(.button-group-outline)::slotted(ion-button.active) {
color: #105cef;
}



// ButtonGroup Medium
// -------------------------------------------------------------------------------

:host(.ion-color-medium) {
background-color: #EFEFEF;
}

:host(.ion-color-medium) .active-indicator {
background-color: #3B3B3B;
}

:host(.ion-color-medium)::slotted(ion-button.active) {
--color: #fff;
}

:host(.ion-color-medium)::slotted(ion-button) {
--color: #242424;
}
110 changes: 110 additions & 0 deletions core/src/components/button-group/button-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { ComponentInterface , EventEmitter} from '@stencil/core';
import { Component, Host, Prop, h, Element, State, Event, Watch } from '@stencil/core';
import { createColorClasses, hostContext } from '@utils/theme';

import { getIonTheme } from '../../global/ionic-global';
import type { Color } from '../../interface';


/**
* @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
* @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component.
*/
@Component({
tag: 'ion-button-group',
styleUrls: {
ios: 'button-group.ionic.scss',
md: 'button-group.ionic.scss',
ionic: 'button-group.ionic.scss',
},
scoped: true,
})
export class ButtonGroup implements ComponentInterface {
@Element() el!: HTMLElement;

@State() activeIndex: number = 0;

@Prop({ reflect: true, mutable: true }) fill?: 'outline' | 'solid';

@Prop({ reflect: true }) shape?: 'soft' | 'round' | 'rectangular';

@Prop({ reflect: true }) size?: 'small' | 'default' | 'large';

@Prop({ reflect: true }) color?: Color;

/**
* The value of the currently selected button.
*/
@Prop({ mutable: true, reflect: true }) value: string | number | null = null;

/**
* Emitted when the active button changes.
*/
@Event() ionChange!: EventEmitter<{ value: string | number | null; activeIndex: number }>;

@Event() ionValueChange!: EventEmitter<{ value: string | number | null}>;

@Watch('value')
valueChanged(value: any | undefined) {
this.ionValueChange.emit({ value });
}

private getButtons(): HTMLIonButtonElement[] {
return Array.from(this.el.querySelectorAll('ion-button'));
}

private handleButtonClick(index: number, value: string | number | null) {
this.activeIndex = index;
this.value = value;
this.ionChange.emit({ value, activeIndex: index });
}

private renderActiveIndicator() {
const buttons = this.getButtons();

const indicatorStyle = {
width: `${100 / buttons.length}%`,
transform: `translateX(${this.activeIndex * 100}%)`,
};

return <div class="active-indicator" style={indicatorStyle} />;
}

componentWillLoad() {
// Initialize the active button based on the value prop
const buttons = Array.from(this.el.querySelectorAll('ion-button'));
const initialIndex = buttons.findIndex((button) => button.getAttribute('value') === `${this.value}`);
if (initialIndex !== -1) {
this.activeIndex = initialIndex;
}
}

render() {
const {color, size, shape, fill} = this;
const theme = getIonTheme(this);
const buttons = this.getButtons();

return (
<Host
class={createColorClasses(color, {
[theme]: true,
[`button-group-${size}`]: size !== undefined,
[`button-group-${shape}`]: true,
[`button-group-${fill}`]: true,
'in-toolbar': hostContext('ion-toolbar', this.el)
})}
>
{this.renderActiveIndicator()}
<slot></slot>
{buttons.map((button, index) => {
button.fill = 'clear';
button.shape = shape;
button.size = size;
button.onclick = () => this.handleButtonClick(index, button.getAttribute('value'));
button.classList.toggle('active', index === this.activeIndex);
button.setAttribute('aria-pressed', index === this.activeIndex ? 'true' : 'false');
})}
</Host>
);
}
}
Loading
Loading