diff --git a/angular/src/directives/navigation/router-controller.ts b/angular/src/directives/navigation/router-controller.ts index 5ebf41f9f6..4156408df2 100644 --- a/angular/src/directives/navigation/router-controller.ts +++ b/angular/src/directives/navigation/router-controller.ts @@ -21,7 +21,7 @@ export class StackController { ref: enteringRef, element: (enteringRef && enteringRef.location && enteringRef.location.nativeElement) as HTMLElement, url: this.getUrl(route), - fullpath: document.location.pathname, + fullpath: document.location!.pathname, deactivatedId: -1 }; } diff --git a/angular/src/directives/navigation/tabs-delegate.ts b/angular/src/directives/navigation/tabs-delegate.ts index e89dd4f2ce..121241d615 100644 --- a/angular/src/directives/navigation/tabs-delegate.ts +++ b/angular/src/directives/navigation/tabs-delegate.ts @@ -15,7 +15,7 @@ export class TabsDelegate { } } - @HostListener('ionTabbarClick', ['$event']) + @HostListener('ionTabButtonClick', ['$event']) onTabbarClick(ev: UIEvent) { const tabElm: HTMLIonTabElement = ev.detail as any; if (this.router && tabElm && tabElm.href) { diff --git a/angular/src/directives/proxies-list.txt b/angular/src/directives/proxies-list.txt index 2e713a7b2b..827390d133 100644 --- a/angular/src/directives/proxies-list.txt +++ b/angular/src/directives/proxies-list.txt @@ -72,7 +72,8 @@ export const DIRECTIVES = [ d.Spinner, d.SplitPane, d.Tab, - d.Tabbar, + d.TabBar, + d.TabButton, d.Tabs, d.Text, d.Textarea, diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index 75aa5f1d82..703cd42d4b 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -813,33 +813,42 @@ export class SplitPane { } export declare interface Tab extends StencilComponents<'IonTab'> {} -@Component({ selector: 'ion-tab', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', inputs: ['active', 'btnId', 'delegate', 'label', 'href', 'icon', 'badge', 'badgeColor', 'component', 'name', 'disabled', 'selected', 'show', 'tabsHideOnSubPages'] }) +@Component({ selector: 'ion-tab', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', inputs: ['active', 'delegate', 'tab', 'component'] }) export class Tab { - ionSelect: EventEmitter; - ionTabMutated: EventEmitter; constructor(r: ElementRef) { const el = r.nativeElement; proxyMethods(this, el, ['setActive']); - proxyInputs(this, el, ['active', 'btnId', 'delegate', 'label', 'href', 'icon', 'badge', 'badgeColor', 'component', 'name', 'disabled', 'selected', 'show', 'tabsHideOnSubPages']); - proxyOutputs(this, el, ['ionSelect', 'ionTabMutated']); + proxyInputs(this, el, ['active', 'delegate', 'tab', 'component']); } } -export declare interface Tabbar extends StencilComponents<'IonTabbar'> {} -@Component({ selector: 'ion-tabbar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', inputs: ['mode', 'color', 'layout', 'placement', 'selectedTab', 'tabs', 'highlight', 'translucent'] }) -export class Tabbar { - ionTabbarClick: EventEmitter; +export declare interface TabBar extends StencilComponents<'IonTabBar'> {} +@Component({ selector: 'ion-tab-bar', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', inputs: ['mode', 'color', 'layout', 'placement', 'selectedTab', 'translucent'] }) +export class TabBar { + ionTabBarChanged: EventEmitter; constructor(r: ElementRef) { const el = r.nativeElement; - proxyInputs(this, el, ['mode', 'color', 'layout', 'placement', 'selectedTab', 'tabs', 'highlight', 'translucent']); - proxyOutputs(this, el, ['ionTabbarClick']); + proxyInputs(this, el, ['mode', 'color', 'layout', 'placement', 'selectedTab', 'translucent']); + proxyOutputs(this, el, ['ionTabBarChanged']); + } +} + +export declare interface TabButton extends StencilComponents<'IonTabButton'> {} +@Component({ selector: 'ion-tab-button', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', inputs: ['mode', 'color', 'layout', 'href', 'tab', 'disabled'] }) +export class TabButton { + ionTabButtonClick: EventEmitter; + + constructor(r: ElementRef) { + const el = r.nativeElement; + proxyInputs(this, el, ['mode', 'color', 'layout', 'href', 'tab', 'disabled']); + proxyOutputs(this, el, ['ionTabButtonClick']); } } export declare interface Tabs extends StencilComponents<'IonTabs'> {} -@Component({ selector: 'ion-tabs', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', inputs: ['name', 'tabbarHidden', 'useRouter'] }) +@Component({ selector: 'ion-tabs', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '', inputs: ['name'] }) export class Tabs { ionChange: EventEmitter; ionNavWillLoad: EventEmitter; @@ -849,7 +858,7 @@ export class Tabs { constructor(r: ElementRef) { const el = r.nativeElement; proxyMethods(this, el, ['select', 'setRouteId', 'getRouteId', 'getTab', 'getSelected']); - proxyInputs(this, el, ['name', 'tabbarHidden', 'useRouter']); + proxyInputs(this, el, ['name']); proxyOutputs(this, el, ['ionChange', 'ionNavWillLoad', 'ionNavWillChange', 'ionNavDidChange']); } } diff --git a/angular/src/ionic-module.ts b/angular/src/ionic-module.ts index 1c46f29db7..8f3f28ba05 100644 --- a/angular/src/ionic-module.ts +++ b/angular/src/ionic-module.ts @@ -81,7 +81,8 @@ const DECLARATIONS = [ d.Spinner, d.SplitPane, d.Tab, - d.Tabbar, + d.TabBar, + d.TabButton, d.Tabs, d.Text, d.Textarea, diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 2cb1f51421..4c04bdccb2 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -55,6 +55,8 @@ import { Side, SpinnerTypes, StyleEvent, + TabbarChangedDetail, + TabbarClickDetail, TabbarLayout, TabbarPlacement, TextFieldTypes, @@ -3704,7 +3706,7 @@ export namespace Components { interface IonRoute { /** - * Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in ion-tabs it actually refers to the name of the `ion-tab` to select. + * Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select. */ 'component': string; /** @@ -3718,7 +3720,7 @@ export namespace Components { } interface IonRouteAttributes extends StencilHTMLAttributes { /** - * Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in ion-tabs it actually refers to the name of the `ion-tab` to select. + * Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select. */ 'component'?: string; /** @@ -4538,146 +4540,13 @@ export namespace Components { 'when'?: string | boolean; } - interface IonTab { - /** - * If `true`, sets the tab as the active tab. - */ - 'active': boolean; - /** - * The badge for the tab. - */ - 'badge'?: string; - /** - * The badge color for the tab button. - */ - 'badgeColor'?: Color; - /** - * hidden - */ - 'btnId'?: string; - /** - * The component to display inside of the tab. - */ - 'component'?: ComponentRef; - /** - * hidden - */ - 'delegate'?: FrameworkDelegate; - /** - * If `true`, the user cannot interact with the tab. Defaults to `false`. - */ - 'disabled': boolean; - /** - * The URL which will be used as the `href` within this tab's button anchor. - */ - 'href'?: string; - /** - * The icon for the tab. - */ - 'icon'?: string; - /** - * The label of the tab. - */ - 'label'?: string; - /** - * The name of the tab. - */ - 'name'?: string; - /** - * If `true`, the tab will be selected. Defaults to `false`. - */ - 'selected': boolean; - /** - * Set the active component for the tab - */ - 'setActive': () => Promise; - /** - * If `true`, the tab button is visible within the tabbar. Defaults to `true`. - */ - 'show': boolean; - /** - * If `true`, hide the tabs on child pages. - */ - 'tabsHideOnSubPages': boolean; - } - interface IonTabAttributes extends StencilHTMLAttributes { - /** - * If `true`, sets the tab as the active tab. - */ - 'active'?: boolean; - /** - * The badge for the tab. - */ - 'badge'?: string; - /** - * The badge color for the tab button. - */ - 'badgeColor'?: Color; - /** - * hidden - */ - 'btnId'?: string; - /** - * The component to display inside of the tab. - */ - 'component'?: ComponentRef; - /** - * hidden - */ - 'delegate'?: FrameworkDelegate; - /** - * If `true`, the user cannot interact with the tab. Defaults to `false`. - */ - 'disabled'?: boolean; - /** - * The URL which will be used as the `href` within this tab's button anchor. - */ - 'href'?: string; - /** - * The icon for the tab. - */ - 'icon'?: string; - /** - * The label of the tab. - */ - 'label'?: string; - /** - * The name of the tab. - */ - 'name'?: string; - /** - * Emitted when the current tab is selected. - */ - 'onIonSelect'?: (event: CustomEvent) => void; - /** - * Emitted when the tab props mutates. Used internally. - */ - 'onIonTabMutated'?: (event: CustomEvent) => void; - /** - * If `true`, the tab will be selected. Defaults to `false`. - */ - 'selected'?: boolean; - /** - * If `true`, the tab button is visible within the tabbar. Defaults to `true`. - */ - 'show'?: boolean; - /** - * If `true`, hide the tabs on child pages. - */ - 'tabsHideOnSubPages'?: boolean; - } - - interface IonTabbar { + interface IonTabBar { /** * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ 'color'?: Color; /** - * If `true`, show the tab highlight bar under the selected tab. - */ - 'highlight': boolean; - /** - * Set the layout of the text and icon in the tabbar. Available options: `"icon-top"`, `"icon-start"`, `"icon-end"`, `"icon-bottom"`, `"icon-hide"`, `"label-hide"`. + * Set the layout of the text and icon in the tabbar. */ 'layout': TabbarLayout; /** @@ -4685,33 +4554,87 @@ export namespace Components { */ 'mode': Mode; /** - * Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`. + * Set the position of the tabbar, relative to the content. */ 'placement': TabbarPlacement; /** * The selected tab component */ - 'selectedTab'?: HTMLIonTabElement; + 'selectedTab'?: string; /** - * The tabs to render - */ - 'tabs': HTMLIonTabElement[]; - /** - * If `true`, the tabbar will be translucent. Defaults to `false`. + * If `true`, the tab bar will be translucent. Defaults to `false`. */ 'translucent': boolean; } - interface IonTabbarAttributes extends StencilHTMLAttributes { + interface IonTabBarAttributes extends StencilHTMLAttributes { /** * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ 'color'?: Color; /** - * If `true`, show the tab highlight bar under the selected tab. + * Set the layout of the text and icon in the tabbar. */ - 'highlight'?: boolean; + 'layout'?: TabbarLayout; /** - * Set the layout of the text and icon in the tabbar. Available options: `"icon-top"`, `"icon-start"`, `"icon-end"`, `"icon-bottom"`, `"icon-hide"`, `"label-hide"`. + * The mode determines which platform styles to use. Possible values are: `"ios"` or `"md"`. + */ + 'mode'?: Mode; + 'onIonTabBarChanged'?: (event: CustomEvent) => void; + /** + * Set the position of the tabbar, relative to the content. + */ + 'placement'?: TabbarPlacement; + /** + * The selected tab component + */ + 'selectedTab'?: string; + /** + * If `true`, the tab bar will be translucent. Defaults to `false`. + */ + 'translucent'?: boolean; + } + + interface IonTabButton { + /** + * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). + */ + 'color'?: Color; + /** + * The selected tab component + */ + 'disabled': boolean; + /** + * The URL which will be used as the `href` within this tab's button anchor. + */ + 'href'?: string; + /** + * Set the layout of the text and icon in the tabbar. + */ + 'layout': TabbarLayout; + /** + * The mode determines which platform styles to use. Possible values are: `"ios"` or `"md"`. + */ + 'mode': Mode; + /** + * A tab id must be provided for each `ion-tab`. It's used internally to reference the selected tab or by the router to switch between them. + */ + 'tab'?: string; + } + interface IonTabButtonAttributes extends StencilHTMLAttributes { + /** + * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). + */ + 'color'?: Color; + /** + * The selected tab component + */ + 'disabled'?: boolean; + /** + * The URL which will be used as the `href` within this tab's button anchor. + */ + 'href'?: string; + /** + * Set the layout of the text and icon in the tabbar. */ 'layout'?: TabbarLayout; /** @@ -4721,23 +4644,40 @@ export namespace Components { /** * Emitted when the tab bar is clicked */ - 'onIonTabbarClick'?: (event: CustomEvent) => void; + 'onIonTabButtonClick'?: (event: CustomEvent) => void; /** - * Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`. + * A tab id must be provided for each `ion-tab`. It's used internally to reference the selected tab or by the router to switch between them. */ - 'placement'?: TabbarPlacement; + 'tab'?: string; + } + + interface IonTab { + 'active': boolean; /** - * The selected tab component + * The component to display inside of the tab. */ - 'selectedTab'?: HTMLIonTabElement; + 'component'?: ComponentRef; + 'delegate'?: FrameworkDelegate; /** - * The tabs to render + * Set the active component for the tab */ - 'tabs'?: HTMLIonTabElement[]; + 'setActive': () => Promise; /** - * If `true`, the tabbar will be translucent. Defaults to `false`. + * A tab id must be provided for each `ion-tab`. It's used internally to reference the selected tab or by the router to switch between them. */ - 'translucent'?: boolean; + 'tab'?: string; + } + interface IonTabAttributes extends StencilHTMLAttributes { + 'active'?: boolean; + /** + * The component to display inside of the tab. + */ + 'component'?: ComponentRef; + 'delegate'?: FrameworkDelegate; + /** + * A tab id must be provided for each `ion-tab`. It's used internally to reference the selected tab or by the router to switch between them. + */ + 'tab'?: string; } interface IonTabs { @@ -4749,7 +4689,7 @@ export namespace Components { /** * Get the tab at the given index */ - 'getTab': (tabOrIndex: string | number | HTMLIonTabElement) => Promise; + 'getTab': (tab: string | HTMLIonTabElement) => Promise; /** * A unique name for the tabs. */ @@ -4757,16 +4697,8 @@ export namespace Components { /** * Index or the Tab instance, of the tab to select. */ - 'select': (tabOrIndex: number | HTMLIonTabElement) => Promise; + 'select': (tab: string | HTMLIonTabElement) => Promise; 'setRouteId': (id: string) => Promise; - /** - * If `true`, the tabbar will be hidden. Defaults to `false`. - */ - 'tabbarHidden': boolean; - /** - * If `true`, the tabs will use the router and `selectedTab` will not do anything. - */ - 'useRouter': boolean; } interface IonTabsAttributes extends StencilHTMLAttributes { /** @@ -4789,14 +4721,6 @@ export namespace Components { * Emitted when the navigation will load a component. */ 'onIonNavWillLoad'?: (event: CustomEvent) => void; - /** - * If `true`, the tabbar will be hidden. Defaults to `false`. - */ - 'tabbarHidden'?: boolean; - /** - * If `true`, the tabs will use the router and `selectedTab` will not do anything. - */ - 'useRouter'?: boolean; } interface IonText { @@ -5456,8 +5380,9 @@ declare global { 'IonSlides': Components.IonSlides; 'IonSpinner': Components.IonSpinner; 'IonSplitPane': Components.IonSplitPane; + 'IonTabBar': Components.IonTabBar; + 'IonTabButton': Components.IonTabButton; 'IonTab': Components.IonTab; - 'IonTabbar': Components.IonTabbar; 'IonTabs': Components.IonTabs; 'IonText': Components.IonText; 'IonTextarea': Components.IonTextarea; @@ -5559,8 +5484,9 @@ declare global { 'ion-slides': Components.IonSlidesAttributes; 'ion-spinner': Components.IonSpinnerAttributes; 'ion-split-pane': Components.IonSplitPaneAttributes; + 'ion-tab-bar': Components.IonTabBarAttributes; + 'ion-tab-button': Components.IonTabButtonAttributes; 'ion-tab': Components.IonTabAttributes; - 'ion-tabbar': Components.IonTabbarAttributes; 'ion-tabs': Components.IonTabsAttributes; 'ion-text': Components.IonTextAttributes; 'ion-textarea': Components.IonTextareaAttributes; @@ -6102,18 +6028,24 @@ declare global { new (): HTMLIonSplitPaneElement; }; + interface HTMLIonTabBarElement extends Components.IonTabBar, HTMLStencilElement {} + var HTMLIonTabBarElement: { + prototype: HTMLIonTabBarElement; + new (): HTMLIonTabBarElement; + }; + + interface HTMLIonTabButtonElement extends Components.IonTabButton, HTMLStencilElement {} + var HTMLIonTabButtonElement: { + prototype: HTMLIonTabButtonElement; + new (): HTMLIonTabButtonElement; + }; + interface HTMLIonTabElement extends Components.IonTab, HTMLStencilElement {} var HTMLIonTabElement: { prototype: HTMLIonTabElement; new (): HTMLIonTabElement; }; - interface HTMLIonTabbarElement extends Components.IonTabbar, HTMLStencilElement {} - var HTMLIonTabbarElement: { - prototype: HTMLIonTabbarElement; - new (): HTMLIonTabbarElement; - }; - interface HTMLIonTabsElement extends Components.IonTabs, HTMLStencilElement {} var HTMLIonTabsElement: { prototype: HTMLIonTabsElement; @@ -6263,8 +6195,9 @@ declare global { 'ion-slides': HTMLIonSlidesElement 'ion-spinner': HTMLIonSpinnerElement 'ion-split-pane': HTMLIonSplitPaneElement + 'ion-tab-bar': HTMLIonTabBarElement + 'ion-tab-button': HTMLIonTabButtonElement 'ion-tab': HTMLIonTabElement - 'ion-tabbar': HTMLIonTabbarElement 'ion-tabs': HTMLIonTabsElement 'ion-text': HTMLIonTextElement 'ion-textarea': HTMLIonTextareaElement @@ -6366,8 +6299,9 @@ declare global { 'ion-slides': HTMLIonSlidesElement; 'ion-spinner': HTMLIonSpinnerElement; 'ion-split-pane': HTMLIonSplitPaneElement; + 'ion-tab-bar': HTMLIonTabBarElement; + 'ion-tab-button': HTMLIonTabButtonElement; 'ion-tab': HTMLIonTabElement; - 'ion-tabbar': HTMLIonTabbarElement; 'ion-tabs': HTMLIonTabsElement; 'ion-text': HTMLIonTextElement; 'ion-textarea': HTMLIonTextareaElement; diff --git a/core/src/components/animation-controller/animation-controller.tsx b/core/src/components/animation-controller/animation-controller.tsx index af645292bb..35e3abb5d5 100644 --- a/core/src/components/animation-controller/animation-controller.tsx +++ b/core/src/components/animation-controller/animation-controller.tsx @@ -4,7 +4,7 @@ import { Animation, AnimationBuilder, AnimationController, Config } from '../../ import { Animator } from './animator'; -/** @hidden */ +/** @internal */ @Component({ tag: 'ion-animation-controller' }) diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx index 3677baefe1..34a5327d7c 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -132,7 +132,6 @@ export class Button implements ComponentInterface { const form = this.el.closest('form'); if (form) { ev.preventDefault(); - ev.stopPropagation(); const fakeButton = document.createElement('button'); fakeButton.type = this.type; diff --git a/core/src/components/item-divider/item-divider.tsx b/core/src/components/item-divider/item-divider.tsx index 75315f4ac3..0903396e27 100644 --- a/core/src/components/item-divider/item-divider.tsx +++ b/core/src/components/item-divider/item-divider.tsx @@ -40,7 +40,10 @@ export class ItemDivider implements ComponentInterface { hostData() { return { - class: createColorClasses(this.color) + class: { + ...createColorClasses(this.color), + 'item': true, + } }; } diff --git a/core/src/components/item-group/item-group.tsx b/core/src/components/item-group/item-group.tsx index 446f19d7a1..571ae027bf 100644 --- a/core/src/components/item-group/item-group.tsx +++ b/core/src/components/item-group/item-group.tsx @@ -16,7 +16,10 @@ export class ItemGroup implements ComponentInterface { hostData() { return { - class: createThemedClasses(this.mode, 'item-group') + class: { + ...createThemedClasses(this.mode, 'item-group'), + 'item': true, + } }; } } diff --git a/core/src/components/label/label.ios.scss b/core/src/components/label/label.ios.scss index 4e699ece43..3c40090a8b 100644 --- a/core/src/components/label/label.ios.scss +++ b/core/src/components/label/label.ios.scss @@ -4,7 +4,7 @@ // iOS Label // -------------------------------------------------- -:host { +:host-context(.item) { @include margin($label-ios-margin-top, $label-ios-margin-end, $label-ios-margin-bottom, $label-ios-margin-start); } diff --git a/core/src/components/label/label.md.scss b/core/src/components/label/label.md.scss index 6206c64d7c..8afbdc5310 100644 --- a/core/src/components/label/label.md.scss +++ b/core/src/components/label/label.md.scss @@ -4,7 +4,7 @@ // Material Design Label // -------------------------------------------------- -:host { +:host-context(.item) { @include margin($label-md-margin-top, $label-md-margin-end, $label-md-margin-bottom, $label-md-margin-start); } diff --git a/core/src/components/label/label.scss b/core/src/components/label/label.scss index 0e6cf2ce1c..b147cc45b6 100644 --- a/core/src/components/label/label.scss +++ b/core/src/components/label/label.scss @@ -4,7 +4,7 @@ // Label // -------------------------------------------------- -:host { +:host-context(.item) { /** * @prop --color: Color of the label */ diff --git a/core/src/components/picker-column/picker-column.tsx b/core/src/components/picker-column/picker-column.tsx index 40aa7e4e5b..d4b090cacc 100644 --- a/core/src/components/picker-column/picker-column.tsx +++ b/core/src/components/picker-column/picker-column.tsx @@ -4,7 +4,6 @@ import { Gesture, GestureDetail, Mode, PickerColumn } from '../../interface'; import { hapticSelectionChanged } from '../../utils'; import { clamp } from '../../utils/helpers'; -/** @hidden */ @Component({ tag: 'ion-picker-column' }) diff --git a/core/src/components/picker-controller/picker-controller.tsx b/core/src/components/picker-controller/picker-controller.tsx index bb83b24136..4d6548cb6b 100644 --- a/core/src/components/picker-controller/picker-controller.tsx +++ b/core/src/components/picker-controller/picker-controller.tsx @@ -3,7 +3,7 @@ import { Component, ComponentInterface, Method, Prop } from '@stencil/core'; import { OverlayController, PickerOptions } from '../../interface'; import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays'; -/** @hidden */ +/** @internal */ @Component({ tag: 'ion-picker-controller' }) diff --git a/core/src/components/route/readme.md b/core/src/components/route/readme.md index dc4913f20d..865d6bfbf4 100644 --- a/core/src/components/route/readme.md +++ b/core/src/components/route/readme.md @@ -9,11 +9,11 @@ Router is a component that can take a component, and render it when the Browser ## Properties -| Property | Attribute | Description | Type | -| ---------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | -| `componentProps` | -- | A key value `{ 'red': true, 'blue': 'white'}` containing props that should be passed to the defined component when rendered. | `undefined \| { [key: string]: any; }` | -| `component` | `component` | Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in ion-tabs it actually refers to the name of the `ion-tab` to select. | `string` | -| `url` | `url` | Relative path that needs to match in order for this route to apply. Accepts paths similar to expressjs so that you can define parameters in the url /foo/:bar where bar would be available in incoming props. | `string` | +| Property | Attribute | Description | Type | +| ---------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | +| `componentProps` | -- | A key value `{ 'red': true, 'blue': 'white'}` containing props that should be passed to the defined component when rendered. | `undefined \| { [key: string]: any; }` | +| `component` | `component` | Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select. | `string` | +| `url` | `url` | Relative path that needs to match in order for this route to apply. Accepts paths similar to expressjs so that you can define parameters in the url /foo/:bar where bar would be available in incoming props. | `string` | ## Events diff --git a/core/src/components/route/route.tsx b/core/src/components/route/route.tsx index 56d9685451..efb9fe5f39 100644 --- a/core/src/components/route/route.tsx +++ b/core/src/components/route/route.tsx @@ -18,7 +18,7 @@ export class Route implements ComponentInterface { * when the route matches. * * The value of this property is not always the tagname of the component to load, - * in ion-tabs it actually refers to the name of the `ion-tab` to select. + * in `ion-tabs` it actually refers to the name of the `ion-tab` to select. */ @Prop() component!: string; diff --git a/core/src/components/router/test/basic/index.html b/core/src/components/router/test/basic/index.html index 3598c0aa52..0a405f1016 100644 --- a/core/src/components/router/test/basic/index.html +++ b/core/src/components/router/test/basic/index.html @@ -105,26 +105,38 @@ class TabsPage extends HTMLElement { connectedCallback() { this.innerHTML = ` - - + - + + + + + inline tab 4 + - + - - inline tab 4 - + + Plain List + + + + + Schedule + + + + + Stopwatch + + + + + Messages + + + + `; } @@ -194,8 +206,8 @@ - - + + diff --git a/core/src/components/tab-bar/readme.md b/core/src/components/tab-bar/readme.md new file mode 100644 index 0000000000..c5fca1c7e2 --- /dev/null +++ b/core/src/components/tab-bar/readme.md @@ -0,0 +1,42 @@ +# ion-tab-bar + +Tab bar is the UI component that implements the array of button of `ion-tabs`. It's provided by default when `ion-tabs` is used, though, this "implicit" tab bar can not be customized. + +In order to have a custom tabbar, it should be provided in user's markup as direct children of `ion-tabs`: + +```html + + + + + + + + + + +``` + + + + + +## Properties + +| Property | Attribute | Description | Type | +| ------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | +| `layout` | `layout` | Set the layout of the text and icon in the tabbar. | `"icon-bottom" \| "icon-end" \| "icon-hide" \| "icon-start" \| "icon-top" \| "label-hide"` | +| `mode` | `mode` | The mode determines which platform styles to use. Possible values are: `"ios"` or `"md"`. | `"ios" \| "md"` | +| `placement` | `placement` | Set the position of the tabbar, relative to the content. | `"bottom" \| "top"` | +| `selectedTab` | `selected-tab` | The selected tab component | `string \| undefined` | +| `translucent` | `translucent` | If `true`, the tab bar will be translucent. Defaults to `false`. | `boolean` | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/core/src/components/tabbar/tabbar-interface.ts b/core/src/components/tab-bar/tab-bar-interface.ts similarity index 54% rename from core/src/components/tabbar/tabbar-interface.ts rename to core/src/components/tab-bar/tab-bar-interface.ts index fb61458253..875e1ab132 100644 --- a/core/src/components/tabbar/tabbar-interface.ts +++ b/core/src/components/tab-bar/tab-bar-interface.ts @@ -1,2 +1,11 @@ export type TabbarLayout = 'icon-top' | 'icon-start' | 'icon-end' | 'icon-bottom' | 'icon-hide' | 'label-hide'; export type TabbarPlacement = 'top' | 'bottom'; + +export interface TabbarChangedDetail { + tab?: string; +} + +export interface TabbarClickDetail { + tab?: string; + href?: string; +} diff --git a/core/src/components/tab-bar/tab-bar.ios.scss b/core/src/components/tab-bar/tab-bar.ios.scss new file mode 100644 index 0000000000..4179ce9bba --- /dev/null +++ b/core/src/components/tab-bar/tab-bar.ios.scss @@ -0,0 +1,21 @@ +@import "./tab-bar"; +@import "../../themes/ionic.globals.ios"; + +// iOS Tabs +// -------------------------------------------------- + +:host { + // default color / background + --background: #{$tabbar-ios-background}; + --border: #{$hairlines-width solid $tabbar-ios-border-color}; + + height: 50px; +} + +// iOS Translucent Tabbar +// -------------------------------------------------- + +:host(.tabbar-translucent) { + background-color: #{current-color(base, .8)}; + backdrop-filter: saturate(210%) blur(20px); +} diff --git a/core/src/components/tab-bar/tab-bar.md.scss b/core/src/components/tab-bar/tab-bar.md.scss new file mode 100644 index 0000000000..95833e78f8 --- /dev/null +++ b/core/src/components/tab-bar/tab-bar.md.scss @@ -0,0 +1,10 @@ +@import "./tab-bar"; +@import "../../themes/ionic.globals.md"; + +:host { + // default color / background + --background: #{$tabbar-md-background}; + --border: #{1px solid $tabbar-md-border-color}; + + height: 56px; +} diff --git a/core/src/components/tab-bar/tab-bar.scss b/core/src/components/tab-bar/tab-bar.scss new file mode 100644 index 0000000000..b1880738c1 --- /dev/null +++ b/core/src/components/tab-bar/tab-bar.scss @@ -0,0 +1,51 @@ +@import "../../themes/ionic.globals"; + +:host { + @include padding-horizontal( + var(--ion-safe-area-left), + var(--ion-safe-area-right) + ); + + display: flex; + + align-items: center; + justify-content: center; + order: 1; + + width: auto; + + background: var(--background); + color: var(--color); + + contain: strict; + user-select: none; + z-index: $z-index-toolbar; + box-sizing: content-box; +} + +:host(.ion-color) { + --background: #{current-color(base)}; +} + +:host(.ion-color) ::slotted(ion-tab-button) { + --color: #{current-color(contrast, .7)}; + --color-selected: #{current-color(contrast)}; + --background-focused: #{current-color(shade)}; +} + +:host(.placement-top) { + order: -1; + + border-bottom: var(--border); +} + +:host(.placement-bottom) { + padding-bottom: var(--ion-safe-area-bottom, 0); + + border-top: var(--border); +} + +:host(.tabbar-hidden) { + /* stylelint-disable-next-line declaration-no-important */ + display: none !important; +} diff --git a/core/src/components/tab-bar/tab-bar.tsx b/core/src/components/tab-bar/tab-bar.tsx new file mode 100644 index 0000000000..7186fbb165 --- /dev/null +++ b/core/src/components/tab-bar/tab-bar.tsx @@ -0,0 +1,101 @@ +import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Prop, QueueApi, State, Watch } from '@stencil/core'; + +import { Color, Mode, TabbarChangedDetail, TabbarLayout, TabbarPlacement } from '../../interface'; +import { createColorClasses } from '../../utils/theme'; + +@Component({ + tag: 'ion-tab-bar', + styleUrls: { + ios: 'tab-bar.ios.scss', + md: 'tab-bar.md.scss' + }, + shadow: true +}) +export class TabBar implements ComponentInterface { + + @Element() el!: HTMLElement; + + @Prop({ context: 'queue' }) queue!: QueueApi; + @Prop({ context: 'document' }) doc!: Document; + + @State() keyboardVisible = false; + + /** + * The mode determines which platform styles to use. + * Possible values are: `"ios"` or `"md"`. + */ + @Prop() mode!: Mode; + + /** + * The color to use from your application's color palette. + * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. + * For more information on colors, see [theming](/docs/theming/basics). + */ + @Prop() color?: Color; + + /** + * Set the layout of the text and icon in the tabbar. + */ + @Prop() layout: TabbarLayout = 'icon-top'; + + /** + * Set the position of the tabbar, relative to the content. + */ + @Prop() placement: TabbarPlacement = 'bottom'; + + /** + * The selected tab component + */ + @Prop() selectedTab?: string; + @Watch('selectedTab') + selectedTabChanged() { + this.ionTabBarChanged.emit({ + tab: this.selectedTab + }); + } + + /** + * If `true`, the tab bar will be translucent. Defaults to `false`. + */ + @Prop() translucent = false; + + /** @internal */ + @Event() ionTabBarChanged!: EventEmitter; + + @Listen('body:keyboardWillHide') + protected onKeyboardWillHide() { + setTimeout(() => this.keyboardVisible = false, 50); + } + + @Listen('body:keyboardWillShow') + protected onKeyboardWillShow() { + if (this.placement === 'bottom') { + this.keyboardVisible = true; + } + } + + componentWillLoad() { + this.selectedTabChanged(); + } + + hostData() { + const { color, translucent, placement, keyboardVisible } = this; + return { + role: 'tablist', + 'aria-hidden': keyboardVisible ? 'true' : null, + 'slot': 'tabbar', + class: { + ...createColorClasses(color), + 'tabbar-translucent': translucent, + [`placement-${placement}`]: true, + 'tabbar-hidden': keyboardVisible, + } + }; + } + + render() { + return ( + + ); + } +} diff --git a/core/src/components/tab-bar/test/scenarios/e2e.ts b/core/src/components/tab-bar/test/scenarios/e2e.ts new file mode 100644 index 0000000000..d60ee190fa --- /dev/null +++ b/core/src/components/tab-bar/test/scenarios/e2e.ts @@ -0,0 +1,10 @@ +import { newE2EPage } from '@stencil/core/testing'; + +it('tab-bar: scenarios', async () => { + const page = await newE2EPage({ + url: '/src/components/tab-bar/test/scenarios?ionic:_testing=true' + }); + + const compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); +}); diff --git a/core/src/components/tab-bar/test/scenarios/index.html b/core/src/components/tab-bar/test/scenarios/index.html new file mode 100644 index 0000000000..03d472cdf3 --- /dev/null +++ b/core/src/components/tab-bar/test/scenarios/index.html @@ -0,0 +1,194 @@ + + + + + + Tab - Colors + + + + + + + + + + + + + + Recents + + + + Favorites + 6 + + + + Settings + + + + + + + + + + + + + + + + + + + + + + + + Location + + + + + 6 + + + + + Radio + + + + + + + + + + + Recents + + + + + 6 + Favorites + + + + + Settings + + + + + + + + + + Recents + + + + + Favorites + + 6 + + + + Settings + + + + + + + + + + Recents + + + + + Favorites + + 6 + + + + Settings + + + + + + + + + + Recents + + + + + Favorites + + 6 + + + + Settings + + + + + + + + + Recents + + 6 + + + + Favorites + + 6 + + + + Settings + + + + + + + + + Indiana Jones and the Raiders of the Lost Ark + + + + Indiana Jones and the Temple of Doom + + + + Indiana Jones and the Last Crusade + + + + + + + + diff --git a/core/src/components/tab-button/readme.md b/core/src/components/tab-button/readme.md new file mode 100644 index 0000000000..f4a6e34e02 --- /dev/null +++ b/core/src/components/tab-button/readme.md @@ -0,0 +1,42 @@ +# ion-tab-bar + +Tab bar is the UI component that implements the array of button of `ion-tabs`. It's provided by default when `ion-tabs` is used, though, this "implicit" tab bar can not be customized. + +In order to have a custom tab bar, it should be provided in user's markup as direct children of `ion-tabs`: + +```html + + + + + + + + + + +``` + + + + + +## Properties + +| Property | Attribute | Description | Type | +| ---------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | +| `disabled` | `disabled` | The selected tab component | `boolean` | +| `href` | `href` | The URL which will be used as the `href` within this tab's button anchor. | `string \| undefined` | +| `layout` | `layout` | Set the layout of the text and icon in the tabbar. | `"icon-bottom" \| "icon-end" \| "icon-hide" \| "icon-start" \| "icon-top" \| "label-hide"` | +| `mode` | `mode` | The mode determines which platform styles to use. Possible values are: `"ios"` or `"md"`. | `"ios" \| "md"` | +| `tab` | `tab` | A tab id must be provided for each `ion-tab`. It's used internally to reference the selected tab or by the router to switch between them. | `string \| undefined` | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/core/src/components/tab-button/tab-button.ios.scss b/core/src/components/tab-button/tab-button.ios.scss new file mode 100644 index 0000000000..59c0c8384c --- /dev/null +++ b/core/src/components/tab-button/tab-button.ios.scss @@ -0,0 +1,72 @@ +@import "./tab-button"; +@import "./tab-button.ios.vars"; + +:host { + --padding-top: #{$tab-button-ios-padding-top}; + --padding-end: #{$tab-button-ios-padding-end}; + --padding-bottom: #{$tab-button-ios-padding-bottom}; + --padding-start: #{$tab-button-ios-padding-start}; + --color: #{$tab-button-ios-text-color}; + --color-selected: #{$tabbar-ios-color-activated}; + --background: transparent; + --background-focused: #{$tabbar-ios-background-focused}; + + max-width: $tab-button-ios-max-width; + + font-size: $tab-button-ios-font-size; +} + +:host(.tab-has-label-only) ::slotted(ion-label) { + @include margin(2px, 0); + + font-size: $tab-button-ios-font-size + 2; + font-size: 14px; + + line-height: 1.1; +} + +::slotted(ion-label) { + @include margin(0, null, 1px, null); + + min-height: $tab-button-ios-font-size + 1; +} + +::slotted(ion-icon) { + @include margin(4px, null, null, null); + + font-size: $tab-button-ios-icon-size; +} + +::slotted(ion-icon::before) { + vertical-align: top; +} + + +// iOS TabButton Layout +// -------------------------------------------------- + +:host(.tab-layout-icon-end) ::slotted(ion-label), +:host(.tab-layout-icon-start) ::slotted(ion-label), +:host(.tab-layout-icon-hide) ::slotted(ion-label) { + margin-top: 2px; + margin-bottom: 2px; + + font-size: 14px; + + line-height: 1.1; +} + +:host(.tab-layout-icon-end) ::slotted(ion-icon), +:host(.tab-layout-icon-start) ::slotted(ion-icon) { + min-width: 24px; + height: 26px; + + margin-top: 2px; + margin-bottom: 1px; + + font-size: 24px; +} + +:host(.tab-layout-label-hide) ::slotted(ion-icon) { + @include margin(0); +} \ No newline at end of file diff --git a/core/src/components/tabbar/tab-button.ios.vars.scss b/core/src/components/tab-button/tab-button.ios.vars.scss similarity index 100% rename from core/src/components/tabbar/tab-button.ios.vars.scss rename to core/src/components/tab-button/tab-button.ios.vars.scss diff --git a/core/src/components/tab-button/tab-button.md.scss b/core/src/components/tab-button/tab-button.md.scss new file mode 100644 index 0000000000..0824e9ed03 --- /dev/null +++ b/core/src/components/tab-button/tab-button.md.scss @@ -0,0 +1,88 @@ +@import "./tab-button"; +@import "./tab-button.md.vars"; + +// Material Design Tab Button +// -------------------------------------------------- + +:host { + --padding-top: #{$tab-button-md-padding-top}; + --padding-end: #{$tab-button-md-padding-end}; + --padding-bottom: #{$tab-button-md-padding-bottom}; + --padding-start: #{$tab-button-md-padding-start}; + --color: #{$tab-button-md-text-color}; + --color-selected: #{$tabbar-md-color-activated}; + --background: transparent; + --background-focused: #{$tabbar-md-background-focused}; + + max-width: 168px; + + font-size: $tab-button-md-font-size; + font-weight: $tab-button-md-font-weight; +} + + +// Material Design Tab Button Text +// -------------------------------------------------- + +::slotted(ion-label) { + @include margin($tab-button-md-text-margin-top, $tab-button-md-text-margin-end, $tab-button-md-text-margin-bottom, $tab-button-md-text-margin-start); + @include transform-origin(center, bottom); + + transition: $tab-button-md-text-transition; + + text-transform: $tab-button-md-text-capitalization; +} + +::slotted(.tab-selected) ::slotted(ion-label) { + transform: #{$tab-button-md-text-transform-active}; + + transition: $tab-button-md-text-transition; +} + +// Material Design Tab Button Icon +// -------------------------------------------------- + +::slotted(ion-icon) { + @include transform-origin(center, center); + + width: $tab-button-md-icon-size; + height: $tab-button-md-icon-size; + + transition: $tab-button-md-icon-transition; + + font-size: $tab-button-md-icon-size; +} + + + +// Material Design TabButton Layout +// -------------------------------------------------- + +:host(.tab-layout-icon-top) ::slotted(ion-label) { + margin-bottom: -2px; +} + +:host(.tab-layout-icon-bottom) ::slotted(ion-label) { + margin-top: -2px; + // --label-transform: transform-origin(center, top); +} + +:host(.tab-selected) ::slotted(ion-label) { + transform: #{$tab-button-md-text-transform-active}; +} + +:host(.tab-selected) ::slotted(ion-icon) { + transform: #{$tab-button-md-icon-transform-active}; +} + +:host(.tab-layout-icon-end.tab-selected) ::slotted(ion-icon) { + transform: #{$tab-button-md-icon-right-transform-active}; +} + +:host(.tab-layout-icon-bottom.tab-selected) ::slotted(ion-icon) { + transform: #{$tab-button-md-icon-bottom-transform-active}; +} + +:host(.tab-layout-icon-start.tab-selected) ::slotted(ion-icon) { + transform: #{$tab-button-md-icon-left-transform-active}; +} diff --git a/core/src/components/tabbar/tab-button.md.vars.scss b/core/src/components/tab-button/tab-button.md.vars.scss similarity index 100% rename from core/src/components/tabbar/tab-button.md.vars.scss rename to core/src/components/tab-button/tab-button.md.vars.scss diff --git a/core/src/components/tab-button/tab-button.scss b/core/src/components/tab-button/tab-button.scss new file mode 100644 index 0000000000..5fce5acd79 --- /dev/null +++ b/core/src/components/tab-button/tab-button.scss @@ -0,0 +1,169 @@ +@import "../../themes/ionic.globals"; + +:host { + --badge-end: 4%; + + flex: 1; + + height: 100%; + + color: var(--color); +} + +a { + @include text-inherit(); + @include margin(0); + @include padding( + var(--padding-top), + var(--padding-end), + var(--padding-bottom), + var(--padding-start), + ); + + display: flex; + position: relative; + + flex-direction: column; + align-items: center; + justify-content: flex-start; + + width: 100%; + height: 100%; + + border: 0; + + outline: none; + + background: var(--background); + + text-decoration: none; + + cursor: pointer; + overflow: hidden; + box-sizing: border-box; + -webkit-user-drag: none; +} + +a:focus-visible { + background: var(--background-focused); +} + +@media (any-hover: hover) { + a:hover { + color: var(--color-selected); + } +} + +:host(.tab-selected) { + color: var(--color-selected); +} + +:host(.tab-hidden) { + /* stylelint-disable-next-line declaration-no-important */ + display: none !important; +} + +:host(.tab-disabled) { + pointer-events: none; + + opacity: .4; +} + +::slotted(ion-label) { + order: 0; +} + +::slotted(ion-icon) { + order: -1; + + height: 1em; +} + +::slotted(ion-label), +::slotted(ion-icon) { + display: block; + + align-self: center; + + min-width: 26px; + max-width: 100%; + + text-overflow: ellipsis; + + white-space: nowrap; + + overflow: hidden; + box-sizing: border-box; +} + +:host(.tab-has-label-only) ::slotted(ion-label) { + white-space: normal; +} + + + +// Tab Badges +// -------------------------------------------------- + +::slotted(ion-badge) { + @include position(null, var(--badge-end), null, null); + @include padding(1px, 6px); + + box-sizing: border-box; + + position: absolute; + + height: auto; + + font-size: 12px; + + line-height: 16px; +} + + +// Tab Button Layout +// -------------------------------------------------- + +:host(.tab-layout-icon-start) a { + flex-direction: row; +} + +:host(.tab-layout-icon-end) a { + flex-direction: row-reverse; +} + +:host(.tab-layout-icon-bottom) a { + flex-direction: column-reverse; +} + +:host(.tab-layout-icon-start) a, +:host(.tab-layout-icon-end) a, +:host(.tab-layout-icon-hide) a, +:host(.tab-layout-label-hide) a, +:host(.tab-has-icon-only) a, +:host(.tab-has-label-only) a { + justify-content: center; +} + +:host(.tab-layout-icon-hide) ::slotted(ion-icon) { + display: none; +} + +:host(.tab-layout-label-hide) ::slotted(ion-label) { + display: none; +} + +:host(.tab-layout-icon-top), +:host(.tab-layout-icon-bottom), +:host(.tab-layout-icon-only), +:host(.tab-layout-label-hide), +:host(.tab-has-icon-only) { + --badge-end: #{calc(50% - 30px)}; +} + +:host(.tab-layout-icon-hide), +:host(.tab-layout-icon-start), +:host(.tab-layout-icon-end), +:host(.tab-has-label-only) { + --badge-end: #{calc(50% - 50px)}; +} diff --git a/core/src/components/tab-button/tab-button.tsx b/core/src/components/tab-button/tab-button.tsx new file mode 100644 index 0000000000..b1b3774e06 --- /dev/null +++ b/core/src/components/tab-button/tab-button.tsx @@ -0,0 +1,128 @@ +import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Prop, QueueApi, State } from '@stencil/core'; + +import { Color, Mode, TabbarClickDetail, TabbarLayout } from '../../interface'; +import { createColorClasses } from '../../utils/theme'; +import { TabbarChangedDetail } from '../tab-bar/tab-bar-interface'; + +@Component({ + tag: 'ion-tab-button', + styleUrls: { + ios: 'tab-button.ios.scss', + md: 'tab-button.md.scss' + }, + shadow: true +}) +export class TabButton implements ComponentInterface { + + @Element() el!: HTMLElement; + + @Prop({ context: 'queue' }) queue!: QueueApi; + @Prop({ context: 'document' }) doc!: Document; + + /** + * The selected tab component + */ + @State() selected = false; + + /** + * The mode determines which platform styles to use. + * Possible values are: `"ios"` or `"md"`. + */ + @Prop() mode!: Mode; + + /** + * The color to use from your application's color palette. + * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. + * For more information on colors, see [theming](/docs/theming/basics). + */ + @Prop() color?: Color; + + /** + * Set the layout of the text and icon in the tabbar. + */ + @Prop() layout: TabbarLayout = 'icon-top'; + + /** + * The URL which will be used as the `href` within this tab's button anchor. + */ + @Prop() href?: string; + + /** + * A tab id must be provided for each `ion-tab`. It's used internally to reference + * the selected tab or by the router to switch between them. + */ + @Prop() tab?: string; + + /** + * The selected tab component + */ + @Prop() disabled = false; + + /** + * Emitted when the tab bar is clicked + * @internal + */ + @Event() ionTabButtonClick!: EventEmitter; + + @Listen('parent:ionTabBarChanged') + onTabbarChanged(ev: CustomEvent) { + this.selected = this.tab === ev.detail.tab; + } + + @Listen('click') + onClick(ev: Event) { + if (!this.disabled) { + this.ionTabButtonClick.emit({ + tab: this.tab, + href: this.href + }); + } + ev.preventDefault(); + } + + componentWillLoad() { + if (this.tab === undefined) { + console.warn('ion-tab-button needs a tab, so it can be selected'); + } + } + + private get hasLabel() { + return !!this.el.querySelector('ion-label'); + } + + private get hasIcon() { + return !!this.el.querySelector('ion-icon'); + } + + hostData() { + const { color, tab, selected, layout, disabled, hasLabel, hasIcon } = this; + return { + 'role': 'tab', + 'ion-activatable': true, + 'aria-selected': selected ? 'true' : null, + 'id': `tab-button-${tab}`, + 'aria-controls': `tab-view-${tab}`, + class: { + ...createColorClasses(color), + + 'tab-selected': selected, + 'tab-disabled': disabled, + 'tab-has-label': hasLabel, + 'tab-has-icon': hasIcon, + 'tab-has-label-only': hasLabel && !hasIcon, + 'tab-has-icon-only': hasIcon && !hasLabel, + [`tab-layout-${layout}`]: true, + } + }; + } + + render() { + const { mode, href } = this; + return ( + + + {mode === 'md' && } + + ); + } +} diff --git a/core/src/components/tab/readme.md b/core/src/components/tab/readme.md index 757f51e5fd..4c41e79d60 100644 --- a/core/src/components/tab/readme.md +++ b/core/src/components/tab/readme.md @@ -12,30 +12,12 @@ See the [Tabs API Docs](../Tabs/) for more details on configuring Tabs. ## Properties -| Property | Attribute | Description | Type | -| -------------------- | ------------------------ | --------------------------------------------------------------------------- | -------------------------------------------------------- | -| `active` | `active` | If `true`, sets the tab as the active tab. | `boolean` | -| `badgeColor` | `badge-color` | The badge color for the tab button. | `string \| undefined` | -| `badge` | `badge` | The badge for the tab. | `string \| undefined` | -| `btnId` | `btn-id` | hidden | `string \| undefined` | -| `component` | `component` | The component to display inside of the tab. | `Function \| HTMLElement \| null \| string \| undefined` | -| `delegate` | -- | hidden | `FrameworkDelegate \| undefined` | -| `disabled` | `disabled` | If `true`, the user cannot interact with the tab. Defaults to `false`. | `boolean` | -| `href` | `href` | The URL which will be used as the `href` within this tab's button anchor. | `string \| undefined` | -| `icon` | `icon` | The icon for the tab. | `string \| undefined` | -| `label` | `label` | The label of the tab. | `string \| undefined` | -| `name` | `name` | The name of the tab. | `string \| undefined` | -| `selected` | `selected` | If `true`, the tab will be selected. Defaults to `false`. | `boolean` | -| `show` | `show` | If `true`, the tab button is visible within the tabbar. Defaults to `true`. | `boolean` | -| `tabsHideOnSubPages` | `tabs-hide-on-sub-pages` | If `true`, hide the tabs on child pages. | `boolean` | - - -## Events - -| Event | Detail | Description | -| --------------- | ------ | ---------------------------------------------------- | -| `ionSelect` | | Emitted when the current tab is selected. | -| `ionTabMutated` | | Emitted when the tab props mutates. Used internally. | +| Property | Attribute | Description | Type | +| ----------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| `active` | `active` | | `boolean` | +| `component` | `component` | The component to display inside of the tab. | `Function \| HTMLElement \| null \| string \| undefined` | +| `delegate` | -- | | `FrameworkDelegate \| undefined` | +| `tab` | `tab` | A tab id must be provided for each `ion-tab`. It's used internally to reference the selected tab or by the router to switch between them. | `string \| undefined` | ## Methods diff --git a/core/src/components/tab/tab.tsx b/core/src/components/tab/tab.tsx index 8a7f43f274..99cd748d8f 100644 --- a/core/src/components/tab/tab.tsx +++ b/core/src/components/tab/tab.tsx @@ -1,6 +1,6 @@ -import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Method, Prop, Watch } from '@stencil/core'; +import { Build, Component, ComponentInterface, Element, Method, Prop } from '@stencil/core'; -import { Color, ComponentRef, FrameworkDelegate } from '../../interface'; +import { ComponentRef, FrameworkDelegate } from '../../interface'; import { attachComponent } from '../../utils/framework-delegate'; @Component({ @@ -13,94 +13,24 @@ export class Tab implements ComponentInterface { private loaded = false; @Element() el!: HTMLIonTabElement; - /** - * If `true`, sets the tab as the active tab. - */ + /** @internal */ @Prop({ mutable: true }) active = false; - /** hidden */ - @Prop() btnId?: string; - - /** hidden */ + /** @internal */ @Prop() delegate?: FrameworkDelegate; /** - * The label of the tab. + * A tab id must be provided for each `ion-tab`. It's used internally to reference + * the selected tab or by the router to switch between them. */ - @Prop() label?: string; - - /** - * The URL which will be used as the `href` within this tab's button anchor. - */ - @Prop() href?: string; - - /** - * The icon for the tab. - */ - @Prop() icon?: string; - - /** - * The badge for the tab. - */ - @Prop() badge?: string; - - /** - * The badge color for the tab button. - */ - @Prop() badgeColor?: Color; + @Prop() tab?: string; /** * The component to display inside of the tab. */ @Prop() component?: ComponentRef; - /** - * The name of the tab. - */ - @Prop({ mutable: true }) name?: string; - - /** - * If `true`, the user cannot interact with the tab. Defaults to `false`. - */ - @Prop() disabled = false; - - /** - * If `true`, the tab will be selected. Defaults to `false`. - */ - @Prop() selected = false; - - @Watch('selected') - selectedChanged(selected: boolean) { - if (selected) { - this.ionSelect.emit(); - } - } - - /** - * If `true`, the tab button is visible within the tabbar. Defaults to `true`. - */ - @Prop() show = true; - - /** - * If `true`, hide the tabs on child pages. - */ - @Prop() tabsHideOnSubPages = false; - - /** - * Emitted when the current tab is selected. - */ - @Event() ionSelect!: EventEmitter; - - /** - * Emitted when the tab props mutates. Used internally. - */ - @Event() ionTabMutated!: EventEmitter; - componentWillLoad() { - // Set default name - if (this.name === undefined && typeof this.component === 'string') { - this.name = this.component; - } if (Build.isDev) { if (this.component !== undefined && this.el.childElementCount > 0) { @@ -109,18 +39,12 @@ export class Tab implements ComponentInterface { ` or` + `- Remove the embedded content inside the ion-tab: `); } - } - } - @Watch('label') - @Watch('href') - @Watch('show') - @Watch('disabled') - @Watch('badge') - @Watch('badgeColor') - @Watch('icon') - onPropChanged() { - this.ionTabMutated.emit(); + if (this.tab === undefined) { + console.error(`Tab views need to have an unique id attribute: + `); + } + } } /** Set the active component for the tab */ @@ -139,11 +63,12 @@ export class Tab implements ComponentInterface { } hostData() { - const { btnId, active, component } = this; + const { tab, active, component } = this; return { - 'aria-labelledby': btnId, - 'aria-hidden': !active ? 'true' : null, 'role': 'tabpanel', + 'aria-hidden': !active ? 'true' : null, + 'aria-labelledby': `tab-button-${tab}`, + 'id': `tab-view-${tab}`, 'class': { 'ion-page': component === undefined, 'tab-hidden': !active diff --git a/core/src/components/tabbar/readme.md b/core/src/components/tabbar/readme.md deleted file mode 100644 index 47befc233f..0000000000 --- a/core/src/components/tabbar/readme.md +++ /dev/null @@ -1,51 +0,0 @@ -# ion-tabbar - -Tabbar is the UI component that implements the array of button of `ion-tabs`. It's provided by default when `ion-tabs` is used, though, this "implicit" tabbar can not be customized. - -In order to have a custom tabbar, it should be provided in user's markup as direct children of `ion-tabs`: - -```html - - - - - - - - - - -``` - - - - - -## Properties - -| Property | Attribute | Description | Type | -| ------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | -| `highlight` | `highlight` | If `true`, show the tab highlight bar under the selected tab. | `boolean` | -| `layout` | `layout` | Set the layout of the text and icon in the tabbar. Available options: `"icon-top"`, `"icon-start"`, `"icon-end"`, `"icon-bottom"`, `"icon-hide"`, `"label-hide"`. | `"icon-bottom" \| "icon-end" \| "icon-hide" \| "icon-start" \| "icon-top" \| "label-hide"` | -| `mode` | `mode` | The mode determines which platform styles to use. Possible values are: `"ios"` or `"md"`. | `"ios" \| "md"` | -| `placement` | `placement` | Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`. | `"bottom" \| "top"` | -| `selectedTab` | -- | The selected tab component | `HTMLIonTabElement \| undefined` | -| `tabs` | -- | The tabs to render | `HTMLIonTabElement[]` | -| `translucent` | `translucent` | If `true`, the tabbar will be translucent. Defaults to `false`. | `boolean` | - - -## Events - -| Event | Detail | Description | -| ---------------- | ----------------- | ----------------------------------- | -| `ionTabbarClick` | HTMLIonTabElement | Emitted when the tab bar is clicked | - - ----------------------------------------------- - -*Built with [StencilJS](https://stenciljs.com/)* diff --git a/core/src/components/tabbar/tab-button.ios.scss b/core/src/components/tabbar/tab-button.ios.scss deleted file mode 100644 index f4e44e7ba2..0000000000 --- a/core/src/components/tabbar/tab-button.ios.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import "./tab-button"; -@import "./tab-button.ios.vars"; - -.tab-btn { - @include padding($tab-button-ios-padding-top, $tab-button-ios-padding-end, $tab-button-ios-padding-bottom, $tab-button-ios-padding-start); - max-width: $tab-button-ios-max-width; -} - -.tab-btn-text { - @include margin(0, null, 1px, null); - - min-height: $tab-button-ios-font-size + 1; -} - -.tab-btn-has-label-only .tab-btn-text { - @include margin(2px, 0); - - font-size: $tab-button-ios-font-size + 2; - font-size: 14px; - - line-height: 1.1; -} - -.tab-btn-icon { - @include margin(4px, null, null, null); - - font-size: $tab-button-ios-icon-size; -} - -.tab-btn-icon::before { - vertical-align: top; -} diff --git a/core/src/components/tabbar/tab-button.md.scss b/core/src/components/tabbar/tab-button.md.scss deleted file mode 100644 index 35f5432ae3..0000000000 --- a/core/src/components/tabbar/tab-button.md.scss +++ /dev/null @@ -1,48 +0,0 @@ -@import "./tab-button"; -@import "./tab-button.md.vars"; - -// Material Design Tab Button -// -------------------------------------------------- - -.tab-btn { - @include padding($tab-button-md-padding-top, $tab-button-md-padding-end, $tab-button-md-padding-bottom, $tab-button-md-padding-start); - - max-width: 168px; -} - -// Material Design Tab Button Text -// -------------------------------------------------- - -.tab-btn-text { - @include margin($tab-button-md-text-margin-top, $tab-button-md-text-margin-end, $tab-button-md-text-margin-bottom, $tab-button-md-text-margin-start); - @include transform-origin(center, bottom); - - transform: var(--label-transform); - - transition: $tab-button-md-text-transition; - - text-transform: $tab-button-md-text-capitalization; -} - -.tab-btn-selected .tab-btn-text { - transform: #{$tab-button-md-text-transform-active}; - - transition: $tab-button-md-text-transition; -} - -// Material Design Tab Button Icon -// -------------------------------------------------- - -.tab-btn-icon { - @include transform-origin(center, center); - - width: $tab-button-md-icon-size; - height: $tab-button-md-icon-size; - - transform: var(--icon-transform); - - transition: $tab-button-md-icon-transition; - - font-size: $tab-button-md-icon-size; -} - diff --git a/core/src/components/tabbar/tab-button.scss b/core/src/components/tabbar/tab-button.scss deleted file mode 100644 index e4ba2e317a..0000000000 --- a/core/src/components/tabbar/tab-button.scss +++ /dev/null @@ -1,134 +0,0 @@ -@import "../../themes/ionic.globals"; - - -.tab-btn { - @include text-inherit(); - @include margin(0); - @include padding(0); - - display: flex; - position: relative; - - flex: 1; - flex-direction: column; - align-items: center; - justify-content: flex-start; - - width: 100%; - height: 100%; - - border: 0; - - outline: none; - - background: transparent; - - text-decoration: none; - - cursor: pointer; - overflow: hidden; - box-sizing: border-box; - -webkit-user-drag: none; -} - -.tab-btn:focus-visible { - background: var(--background-focused); -} - -@media (any-hover: hover) { - .tab-btn:hover { - color: var(--color-selected); - } -} - -.tab-btn-selected { - color: var(--color-selected); -} - -.tab-btn-hidden { - /* stylelint-disable-next-line declaration-no-important */ - display: none !important; -} - -.tab-btn-disabled { - pointer-events: none; - - opacity: .4; -} - -.tab-btn-text { - @include margin(var(--label-margin-top), null, var(--label-margin-bottom), null); - - display: var(--label-display, block); - - font-size: var(--label-font-size); - - line-height: var(--label-line-height); -} - -.tab-btn-icon { - @include margin(var(--icon-margin-top), null, var(--icon-margin-bottom), null); - - display: var(--icon-display, block); - - min-width: var(--icon-min-width); - height: var(--icon-height, 1em); - - font-size: var(--icon-font-size); -} - -.tab-btn-text, -.tab-btn-icon { - align-self: center; - - min-width: 26px; - max-width: 100%; - - text-overflow: ellipsis; - - white-space: nowrap; - - overflow: hidden; - box-sizing: border-box; -} - -.tab-btn-has-label-only .tab-btn-text { - white-space: normal; -} - -.tab-btn-has-icon-only, -.tab-btn-has-label-only { - justify-content: center; -} - - -// Tab Badges -// -------------------------------------------------- - -.tab-btn-badge { - @include position(6%, 4%, null, null); // 4% fallback - @include position(null, calc(50% - 30px), null, null); - @include padding(1px, 6px); - - box-sizing: border-box; - - position: absolute; - - height: auto; - - font-size: 12px; - - line-height: 16px; -} - -.tab-btn-has-label-only .tab-btn-badge { - @include position-horizontal(null, #{calc(50% - 50px)}); -} - -.tab-btn-has-icon-only .tab-btn-badge { - @include position-horizontal(null, #{calc(50% - 30px)}); -} - -.tab-btn-selected .tab-btn-icon { - transform: var(--icon-transform-selected); -} diff --git a/core/src/components/tabbar/tabbar.ios.scss b/core/src/components/tabbar/tabbar.ios.scss deleted file mode 100644 index da5ef10c60..0000000000 --- a/core/src/components/tabbar/tabbar.ios.scss +++ /dev/null @@ -1,63 +0,0 @@ -@import "./tabbar"; -@import "./tabbar.ios.vars"; -@import "./tab-button.ios"; - -// iOS Tabs -// -------------------------------------------------- - -:host { - // default color / background - --background: #{$tabbar-ios-background}; - --color: #{$tab-button-ios-text-color}; - --color-selected: #{$tabbar-ios-color-activated}; - --background-focused: #{$tabbar-ios-background-focused}; - - justify-content: center; - - height: $tabbar-ios-height; - - border-top: $tabbar-ios-border; - - font-size: $tab-button-ios-font-size; - - contain: strict; -} - -:host(.placement-top) { - border-top: 0; - border-bottom: $tabbar-ios-border; -} - -// iOS Translucent Tabbar -// -------------------------------------------------- - -:host(.tabbar-translucent) { - background-color: #{current-color(base, .8)}; - backdrop-filter: $tabbar-ios-translucent-filter; -} - - -// iOS Tabbar Layout -// -------------------------------------------------- - -:host(.layout-icon-end), -:host(.layout-icon-start), -:host(.layout-icon-hide) { - --label-margin-top: 2px; - --label-margin-bottom: 2px; - --label-font-size: 14px; - --label-line-height: 1.1; -} - -:host(.layout-icon-end), -:host(.layout-icon-start) { - --icon-margin-top: 2px; - --icon-margin-bottom: 1px; - --icon-min-width: 24px; - --icon-height: 26px; - --icon-font-size: 24px; -} - -:host(.layout-label-hide) { - --icon-margin: 0; -} \ No newline at end of file diff --git a/core/src/components/tabbar/tabbar.ios.vars.scss b/core/src/components/tabbar/tabbar.ios.vars.scss deleted file mode 100644 index c4d441705d..0000000000 --- a/core/src/components/tabbar/tabbar.ios.vars.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import "../../themes/ionic.globals.ios"; - -// iOS Tabs -// -------------------------------------------------- - -/// @prop - Height of the tabbar -$tabbar-ios-height: 50px !default; - -/// @prop - Border on the tabbar (border-top when [tabsPlacement=bottom] and border-bottom when [tabsPlacement=top]) -$tabbar-ios-border: $hairlines-width solid $tabbar-ios-border-color !default; - -/// @prop - Filter of the translucent tabbar -$tabbar-ios-translucent-filter: saturate(210%) blur(20px) !default; diff --git a/core/src/components/tabbar/tabbar.md.scss b/core/src/components/tabbar/tabbar.md.scss deleted file mode 100644 index b5399c8c08..0000000000 --- a/core/src/components/tabbar/tabbar.md.scss +++ /dev/null @@ -1,42 +0,0 @@ -@import "./tabbar"; -@import "./tabbar.md.vars"; -@import "./tab-button.md"; - -:host { - // default color / background - --color: #{$tab-button-md-text-color}; - --color-selected: #{$tabbar-md-color-activated}; - --background: #{$tabbar-md-background}; - --background-focused: #{$tabbar-md-background-focused}; - --icon-transform-selected: #{$tab-button-md-icon-transform-active}; - - height: $tabbar-md-height; - - border-top: $tabbar-md-border; - - font-size: $tab-button-md-font-size; - font-weight: $tab-button-md-font-weight; - - contain: strict; -} - -// Material Design Tabbar Layout -// -------------------------------------------------- - -:host(.layout-icon-top) { - --label-margin-bottom: -2px; -} - -:host(.layout-icon-end) { - --icon-transform-selected: #{$tab-button-md-icon-right-transform-active}; -} - -:host(.layout-icon-bottom) { - --label-margin-top: -2px; - --label-transform: transform-origin(center, top); - --icon-transform-selected: #{$tab-button-md-icon-bottom-transform-active}; -} - -:host(.layout-icon-start) { - --icon-transform-selected: #{$tab-button-md-icon-left-transform-active}; -} diff --git a/core/src/components/tabbar/tabbar.md.vars.scss b/core/src/components/tabbar/tabbar.md.vars.scss deleted file mode 100644 index a4aad29697..0000000000 --- a/core/src/components/tabbar/tabbar.md.vars.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import "../../themes/ionic.globals.md"; - -// Material Design Tabs -// -------------------------------------------------- - -/// @prop - Minimum height of the tabbar -$tabbar-md-height: 56px !default; - -/// @prop - Border on the tabbar -$tabbar-md-border: 1px solid $tabbar-md-border-color !default; diff --git a/core/src/components/tabbar/tabbar.scss b/core/src/components/tabbar/tabbar.scss deleted file mode 100644 index 596636de85..0000000000 --- a/core/src/components/tabbar/tabbar.scss +++ /dev/null @@ -1,119 +0,0 @@ -@import "../../themes/ionic.globals"; - -:host { - @include padding-horizontal( - var(--ion-safe-area-left), - var(--ion-safe-area-right) - ); - - display: flex; - - align-items: center; - justify-content: center; - order: 1; - - width: auto; - - background: var(--background); - color: var(--color); - - user-select: none; - - z-index: $z-index-toolbar; - box-sizing: content-box; -} - -:host(.ion-color) { - --background: #{current-color(base)}; - --color: #{current-color(contrast, .7)}; - --color-selected: #{current-color(contrast)}; -} - -:host(.tabbar-hidden) { - /* stylelint-disable-next-line declaration-no-important */ - display: none !important; -} - -:host(.placement-top) { - order: -1; -} - -:host(.placement-bottom) { - padding-bottom: var(--ion-safe-area-bottom, 0); -} - - -// Tab Highlight -// -------------------------------------------------- - -.tabbar-highlight { - @include position(null, null, 0, 0); - @include transform-origin(0, 0); - - display: block; - position: absolute; - - width: 1px; - height: 2px; - - transform: translateZ(0); - - background: currentColor; - - &.animated { - transition-duration: 300ms; - transition-property: transform; - transition-timing-function: cubic-bezier(.4, 0, .2, 1); - will-change: transform; - } -} - -:host(.placement-top) .tabbar-highlight { - bottom: 0; -} - -:host(.placement-bottom) .tabbar-highlight { - top: 0; -} - - -// Tab Layout -// -------------------------------------------------- - -:host(.layout-icon-start) .tab-btn { - flex-direction: row; -} - -:host(.layout-icon-end) .tab-btn { - flex-direction: row-reverse; -} - -:host(.layout-icon-bottom) .tab-btn { - flex-direction: column-reverse; -} - -:host(.layout-icon-start) .tab-btn, -:host(.layout-icon-end) .tab-btn, -:host(.layout-icon-hide) .tab-btn, -:host(.layout-label-hide) .tab-btn { - justify-content: center; -} - -:host(.layout-icon-hide) { - --icon-display: none; -} - -:host(.layout-label-hide) { - --label-display: none; -} - -:host(.layout-icon-top) .tab-btn, -:host(.layout-icon-bottom) .tab-btn { - --badge-end: #{calc(50% - 30px)}; -} - -:host(.layout-icon-hide) .tab-btn, -:host(.layout-icon-start) .tab-btn, -:host(.layout-icon-end) .tab-btn { - --badge-end: #{calc(50% - 50px)}; -} diff --git a/core/src/components/tabbar/tabbar.tsx b/core/src/components/tabbar/tabbar.tsx deleted file mode 100644 index cd89a6bc78..0000000000 --- a/core/src/components/tabbar/tabbar.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Prop, QueueApi, State, Watch } from '@stencil/core'; - -import { Color, Mode, TabbarLayout, TabbarPlacement } from '../../interface'; -import { createColorClasses } from '../../utils/theme'; - -@Component({ - tag: 'ion-tabbar', - styleUrls: { - ios: 'tabbar.ios.scss', - md: 'tabbar.md.scss' - }, - scoped: true -}) -export class Tabbar implements ComponentInterface { - - /** - * The mode determines which platform styles to use. - * Possible values are: `"ios"` or `"md"`. - */ - @Prop() mode!: Mode; - - /** - * The color to use from your application's color palette. - * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. - * For more information on colors, see [theming](/docs/theming/basics). - */ - @Prop() color?: Color; - - @Element() el!: HTMLElement; - - @Prop({ context: 'queue' }) queue!: QueueApi; - @Prop({ context: 'document' }) doc!: Document; - - @State() canScrollLeft = false; - @State() canScrollRight = false; - @State() keyboardVisible = false; - - /** - * Set the layout of the text and icon in the tabbar. Available options: `"icon-top"`, `"icon-start"`, `"icon-end"`, `"icon-bottom"`, `"icon-hide"`, `"label-hide"`. - */ - @Prop() layout: TabbarLayout = 'icon-top'; - - /** - * Set the position of the tabbar, relative to the content. Available options: `"top"`, `"bottom"`. - */ - @Prop() placement: TabbarPlacement = 'bottom'; - - /** - * The selected tab component - */ - @Prop() selectedTab?: HTMLIonTabElement; - - /** - * The tabs to render - */ - @Prop() tabs: HTMLIonTabElement[] = []; - - /** - * If `true`, show the tab highlight bar under the selected tab. - */ - @Prop() highlight = false; - - /** - * If `true`, the tabbar will be translucent. Defaults to `false`. - */ - @Prop() translucent = false; - - /** - * Emitted when the tab bar is clicked - */ - @Event() ionTabbarClick!: EventEmitter; - - @Listen('body:keyboardWillHide') - protected onKeyboardWillHide() { - setTimeout(() => this.keyboardVisible = false, 50); - } - - @Listen('body:keyboardWillShow') - protected onKeyboardWillShow() { - if (this.placement === 'bottom') { - this.keyboardVisible = true; - } - } - - componentDidLoad() { - this.updateHighlight(); - } - - @Watch('selectedTab') - @Listen('window:resize') - private updateHighlight() { - if (!this.highlight) { - return; - } - this.queue.read(() => { - const btn = this.el.shadowRoot!.querySelector('.tab-btn-selected') as HTMLElement | null; - const highlight = this.el.shadowRoot!.querySelector('.tabbar-highlight') as HTMLElement; - if (btn && highlight) { - highlight.style.transform = `translate3d(${btn.offsetLeft}px,0,0) scaleX(${btn.offsetWidth})`; - } - }); - } - - hostData() { - const { color, translucent, layout, placement, keyboardVisible } = this; - return { - role: 'tablist', - 'aria-hidden': keyboardVisible ? 'true' : null, - 'slot': 'tabbar', - class: { - ...createColorClasses(color), - 'tabbar-translucent': translucent, - [`layout-${layout}`]: true, - [`placement-${placement}`]: true, - 'tabbar-hidden': keyboardVisible, - } - }; - } - - renderTabButton(tab: HTMLIonTabElement) { - const { icon, label, disabled, badge, badgeColor, href } = tab; - const selected = tab === this.selectedTab; - const hasLabel = label !== undefined; - const hasIcon = icon !== undefined; - return ( - { - if (!tab.disabled) { - this.ionTabbarClick.emit(tab); - } - ev.preventDefault(); - }} - > - {icon && } - {label && {label}} - {badge && {badge}} - {this.mode === 'md' && } - - ); - } - - render() { - return [ - this.tabs.map(tab => this.renderTabButton(tab)), - this.highlight &&
- ]; - } -} diff --git a/core/src/components/tabs/readme.md b/core/src/components/tabs/readme.md index 64b19af625..533ff6b701 100644 --- a/core/src/components/tabs/readme.md +++ b/core/src/components/tabs/readme.md @@ -4,23 +4,29 @@ Tabs are a top level navigation component for created multiple stacked navs. The component is a container of individual [Tab](../Tab/) components. `ion-tabs` is a styleless component that works as a router outlet in -order to handle navigation. When the user does not provide a `ion-tabbar` in their markup, `ion-tabs`, by default provides one. Notice that `ion-tabbar` is the UI component that can be used to switch between tabs. +order to handle navigation. When the user does not provide a `ion-tab-bar` in their markup, `ion-tabs`, by default provides one. Notice that `ion-tab-bar` is the UI component that can be used to switch between tabs. -In order to customize the style of the `ion-tabbar`, it should be included in the user's markup as +In order to customize the style of the `ion-tab-bar`, it should be included in the user's markup as direct children of `ion-tabs`, like this: ```html - - - - + Home Content + Settings Content - + + + + Home + + + + + Settings + + + + ``` @@ -29,11 +35,9 @@ direct children of `ion-tabs`, like this: ## Properties -| Property | Attribute | Description | Type | -| -------------- | --------------- | ------------------------------------------------------------------------------- | --------------------- | -| `name` | `name` | A unique name for the tabs. | `string \| undefined` | -| `tabbarHidden` | `tabbar-hidden` | If `true`, the tabbar will be hidden. Defaults to `false`. | `boolean` | -| `useRouter` | `use-router` | If `true`, the tabs will use the router and `selectedTab` will not do anything. | `boolean` | +| Property | Attribute | Description | Type | +| -------- | --------- | --------------------------- | --------------------- | +| `name` | `name` | A unique name for the tabs. | `string \| undefined` | ## Events @@ -68,15 +72,15 @@ Type: `Promise` -### `getTab(tabOrIndex: string | number | HTMLIonTabElement) => Promise` +### `getTab(tab: string | HTMLIonTabElement) => Promise` Get the tab at the given index #### Parameters -| Name | Type | Description | -| ------------ | --------------------------------------- | ----------- | -| `tabOrIndex` | `HTMLIonTabElement \| number \| string` | | +| Name | Type | Description | +| ----- | ----------------------------- | ----------- | +| `tab` | `HTMLIonTabElement \| string` | | #### Returns @@ -84,15 +88,15 @@ Type: `Promise` -### `select(tabOrIndex: number | HTMLIonTabElement) => Promise` +### `select(tab: string | HTMLIonTabElement) => Promise` Index or the Tab instance, of the tab to select. #### Parameters -| Name | Type | Description | -| ------------ | ----------------------------- | ----------- | -| `tabOrIndex` | `HTMLIonTabElement \| number` | | +| Name | Type | Description | +| ----- | ----------------------------- | ----------- | +| `tab` | `HTMLIonTabElement \| string` | | #### Returns diff --git a/core/src/components/tabs/tabs.scss b/core/src/components/tabs/tabs.scss index 3f075deac2..969f283f51 100644 --- a/core/src/components/tabs/tabs.scss +++ b/core/src/components/tabs/tabs.scss @@ -22,8 +22,3 @@ contain: layout size style; } - -:host(.tabbar-hidden) ion-tabbar, -:host(.tabbar-hidden)::slotted(ion-tabbar) { - display: none; -} diff --git a/core/src/components/tabs/tabs.tsx b/core/src/components/tabs/tabs.tsx index d39046e4ac..683e37febb 100644 --- a/core/src/components/tabs/tabs.tsx +++ b/core/src/components/tabs/tabs.tsx @@ -1,6 +1,6 @@ -import { Build, Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core'; -import { Config, NavOutlet, RouteID, RouteWrite } from '../../interface'; +import { Config, NavOutlet, RouteID, RouteWrite, TabbarClickDetail } from '../../interface'; @Component({ tag: 'ion-tabs', @@ -9,11 +9,9 @@ import { Config, NavOutlet, RouteID, RouteWrite } from '../../interface'; }) export class Tabs implements NavOutlet { - private ids = -1; private transitioning = false; - private tabsId = (++tabIds); private leavingTab?: HTMLIonTabElement; - private userTabbarEl?: HTMLIonTabbarElement; + private useRouter = false; @Element() el!: HTMLStencilElement; @@ -28,16 +26,6 @@ export class Tabs implements NavOutlet { */ @Prop() name?: string; - /** - * If `true`, the tabbar will be hidden. Defaults to `false`. - */ - @Prop() tabbarHidden = false; - - /** - * If `true`, the tabs will use the router and `selectedTab` will not do anything. - */ - @Prop({ mutable: true }) useRouter = false; - /** * Emitted when the tab changes. */ @@ -59,13 +47,8 @@ export class Tabs implements NavOutlet { @Event() ionNavDidChange!: EventEmitter; async componentWillLoad() { - if (!this.useRouter) { - this.useRouter = !!this.doc.querySelector('ion-router') && !this.el.closest('[no-router]'); - } - this.userTabbarEl = this.el.querySelector('ion-tabbar') || undefined; - - this.initTabs(); - + this.useRouter = !!this.doc.querySelector('ion-router') && !this.el.closest('[no-router]'); + this.tabs = Array.from(this.el.querySelectorAll('ion-tab')); this.ionNavWillLoad.emit(); this.componentWillUpdate(); } @@ -80,28 +63,23 @@ export class Tabs implements NavOutlet { } componentWillUpdate() { - const tabbarEl = this.userTabbarEl; - if (tabbarEl) { - tabbarEl.tabs = this.tabs.slice(); - tabbarEl.selectedTab = this.selectedTab; + const tabbar = this.el.querySelector('ion-tab-bar'); + if (tabbar) { + const tab = this.selectedTab ? this.selectedTab.tab : undefined; + tabbar.selectedTab = tab; } } - @Listen('ionTabMutated') - protected onTabMutated() { - this.el.forceUpdate(); - } - - @Listen('ionTabbarClick') - protected onTabClicked(ev: CustomEvent) { - const selectedTab = ev.detail; - const href = selectedTab.href as string | undefined; + @Listen('ionTabButtonClick') + protected onTabClicked(ev: CustomEvent) { + const { href, tab } = ev.detail; + const selectedTab = this.tabs.find(t => t.tab === tab); if (this.useRouter && href !== undefined) { const router = this.doc.querySelector('ion-router'); if (router) { router.push(href); } - } else { + } else if (selectedTab) { this.select(selectedTab); } } @@ -110,8 +88,8 @@ export class Tabs implements NavOutlet { * Index or the Tab instance, of the tab to select. */ @Method() - async select(tabOrIndex: number | HTMLIonTabElement): Promise { - const selectedTab = await this.getTab(tabOrIndex); + async select(tab: string | HTMLIonTabElement): Promise { + const selectedTab = await this.getTab(tab); if (!this.shouldSwitch(selectedTab)) { return false; } @@ -141,20 +119,21 @@ export class Tabs implements NavOutlet { /** @internal */ @Method() async getRouteId(): Promise { - const id = this.selectedTab && this.selectedTab.name; - return id !== undefined ? { id, element: this.selectedTab } : undefined; + const tabId = this.selectedTab && this.selectedTab.tab; + return tabId !== undefined ? { id: tabId, element: this.selectedTab } : undefined; } /** Get the tab at the given index */ @Method() - async getTab(tabOrIndex: string | number | HTMLIonTabElement): Promise { - if (typeof tabOrIndex === 'string') { - return this.tabs.find(tab => tab.name === tabOrIndex); + async getTab(tab: string | HTMLIonTabElement): Promise { + const tabEl = (typeof tab === 'string') + ? this.tabs.find(t => t.tab === tab) + : tab; + + if (!tabEl) { + console.error(`tab with id: "${tabEl}" does not exist`); } - if (typeof tabOrIndex === 'number') { - return this.tabs[tabOrIndex]; - } - return tabOrIndex; + return tabEl; } /** @@ -165,47 +144,13 @@ export class Tabs implements NavOutlet { return Promise.resolve(this.selectedTab); } - private initTabs() { - const tabs = this.tabs = Array.from(this.el.querySelectorAll('ion-tab')); - tabs.forEach(tab => { - const id = `t-${this.tabsId}-${++this.ids}`; - tab.btnId = 'tab-' + id; - tab.id = 'tabpanel-' + id; - }); - } - private async initSelect(): Promise { - const tabs = this.tabs; - // wait for all tabs to be ready - await Promise.all(tabs.map(tab => tab.componentOnReady())); if (this.useRouter) { - if (Build.isDev) { - const tab = tabs.find(t => t.selected); - if (tab) { - console.warn('When using a router (ion-router) makes no difference' + - 'Define routes properly the define which tab is selected'); - } - } return; } - // find pre-selected tabs - const selectedTab = tabs.find(t => t.selected) || - tabs.find(t => t.show && !t.disabled); - - // reset all tabs none is selected - for (const tab of tabs) { - if (tab !== selectedTab) { - tab.selected = false; - } - } - if (selectedTab) { - await selectedTab.setActive(); - } - this.selectedTab = selectedTab; - if (selectedTab) { - selectedTab.selected = true; - selectedTab.active = true; - } + // wait for all tabs to be ready + await Promise.all(this.tabs.map(tab => tab.componentOnReady())); + await this.select(this.tabs[0]); } private setActive(selectedTab: HTMLIonTabElement): Promise { @@ -213,13 +158,6 @@ export class Tabs implements NavOutlet { return Promise.reject('transitioning already happening'); } - // Reset rest of tabs - for (const tab of this.tabs) { - if (selectedTab !== tab) { - tab.selected = false; - } - } - this.transitioning = true; this.leavingTab = this.selectedTab; this.selectedTab = selectedTab; @@ -237,7 +175,6 @@ export class Tabs implements NavOutlet { return; } - selectedTab.selected = true; if (leavingTab !== selectedTab) { if (leavingTab) { leavingTab.active = false; @@ -262,28 +199,12 @@ export class Tabs implements NavOutlet { return selectedTab !== undefined && selectedTab !== leavingTab && !this.transitioning; } - hostData() { - return { - class: { - 'tabbar-hidden': this.tabbarHidden - } - }; - } - render() { return [
, - - - - + ]; } } - -let tabIds = -1; diff --git a/core/src/components/tabs/test/basic/e2e.js b/core/src/components/tabs/test/basic/e2e.js deleted file mode 100644 index b6c656dc2d..0000000000 --- a/core/src/components/tabs/test/basic/e2e.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const { register, Page, platforms } = require('../../../../../scripts/e2e'); -const { getElement, waitAndGetElementById, waitForTransition } = require('../../../../../scripts/e2e/utils'); - -class E2ETestPage extends Page { - constructor(driver, platform) { - super(driver, `http://localhost:3333/src/components/tabs/test/basic?ionic:mode=${platform}`); - } -} - -platforms.forEach(platform => { - describe('tabs/basic', () => { - register('should init', driver => { - const page = new E2ETestPage(driver, platform); - return page.navigate(); - }); - - // register('should check each tab', async (driver, testContext) => { - // testContext.timeout(60000); - // const page = new E2ETestPage(driver, platform); - - // await waitForTransition(300); - - // const tabTwoButton = await waitAndGetElementById(driver, 'tab-t-0-1'); - // tabTwoButton.click(); - // await waitForTransition(600); - - // const tabThreeButton = await waitAndGetElementById(driver, 'tab-t-0-2'); - // tabThreeButton.click(); - // await waitForTransition(600); - // }); - }); -}); diff --git a/core/src/components/tabs/test/basic/e2e.ts b/core/src/components/tabs/test/basic/e2e.ts new file mode 100644 index 0000000000..ba03f6f8dc --- /dev/null +++ b/core/src/components/tabs/test/basic/e2e.ts @@ -0,0 +1,25 @@ +import { newE2EPage } from '@stencil/core/testing'; + +it('tab-group: basic', async () => { + const page = await newE2EPage({ + url: '/src/components/tab-group/test/basic?ionic:_testing=true' + }); + + let compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); + + const button2 = await page.find('.e2eTabTwoButton'); + await button2.click(); + compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); + + const button3 = await page.find('.e2eTabThreeButton'); + await button3.click(); + compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); + + const button4 = await page.find('.e2eTabFourButton'); + await button4.click(); + compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); +}); diff --git a/core/src/components/tabs/test/basic/index.html b/core/src/components/tabs/test/basic/index.html index dd063848a7..9a3fd23ce4 100644 --- a/core/src/components/tabs/test/basic/index.html +++ b/core/src/components/tabs/test/basic/index.html @@ -14,7 +14,8 @@ - + + Tab One @@ -22,13 +23,12 @@

Tab One

- Update Badge Count Update Badge Color
- + Tab Two @@ -39,7 +39,7 @@ - + Tab Three @@ -50,7 +50,7 @@ - + Hidden Tab @@ -61,25 +61,58 @@ - - + - + + + Plain List + + + + + Schedule + + 6 + + + + Stopwatch + + 6 + + + + + + Messages + + + +
diff --git a/core/src/components/tabs/test/colors/e2e.js b/core/src/components/tabs/test/colors/e2e.js deleted file mode 100644 index 7c10f08c89..0000000000 --- a/core/src/components/tabs/test/colors/e2e.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const { register, Page, platforms } = require('../../../../../scripts/e2e'); -const { getElement, waitAndGetElementById, waitForTransition } = require('../../../../../scripts/e2e/utils'); - -class E2ETestPage extends Page { - constructor(driver, platform) { - super(driver, `http://localhost:3333/src/components/tabs/test/colors?ionic:mode=${platform}`); - } -} - -platforms.forEach(platform => { - describe('tabs/colors', () => { - register('should init', driver => { - const page = new E2ETestPage(driver, platform); - return page.navigate(); - }); - - // register('should check each tab', async (driver, testContext) => { - // testContext.timeout(60000); - // const page = new E2ETestPage(driver, platform); - - // await waitForTransition(300); - - // const tabTwoButton = await waitAndGetElementById(driver, 'tab-t-0-1'); - // tabTwoButton.click(); - // await waitForTransition(600); - - // const tabThreeButton = await waitAndGetElementById(driver, 'tab-t-0-2'); - // tabThreeButton.click(); - // await waitForTransition(600); - // }); - }); -}); diff --git a/core/src/components/tabs/test/colors/index.html b/core/src/components/tabs/test/colors/index.html deleted file mode 100644 index 0a925fad67..0000000000 --- a/core/src/components/tabs/test/colors/index.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - Tab - Colors - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/src/components/tabs/test/nav/index.html b/core/src/components/tabs/test/nav/index.html index 3e46943415..4493fadcca 100644 --- a/core/src/components/tabs/test/nav/index.html +++ b/core/src/components/tabs/test/nav/index.html @@ -252,19 +252,36 @@ - + - + - + - + + + + + Plain List + + + + + Schedule + + + + + Stopwatch + + + diff --git a/core/src/components/tabs/test/placements/e2e.js b/core/src/components/tabs/test/placements/e2e.js deleted file mode 100644 index e326923ba7..0000000000 --- a/core/src/components/tabs/test/placements/e2e.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const { register, Page, platforms } = require('../../../../../scripts/e2e'); -const { getElement, waitAndGetElementById, waitForTransition } = require('../../../../../scripts/e2e/utils'); - -class E2ETestPage extends Page { - constructor(driver, platform) { - super(driver, `http://localhost:3333/src/components/tabs/test/placements?ionic:mode=${platform}`); - } -} - -platforms.forEach(platform => { - describe('tabs/placements', () => { - register('should init', driver => { - const page = new E2ETestPage(driver, platform); - return page.navigate(); - }); - - // register('should check each tab', async (driver, testContext) => { - // testContext.timeout(60000); - // const page = new E2ETestPage(driver, platform); - - // await waitForTransition(300); - - // const tabTwoButton = await waitAndGetElementById(driver, 'tab-t-0-1'); - // tabTwoButton.click(); - // await waitForTransition(600); - - // const tabThreeButton = await waitAndGetElementById(driver, 'tab-t-0-2'); - // tabThreeButton.click(); - // await waitForTransition(600); - // }); - }); -}); diff --git a/core/src/components/tabs/test/placements/e2e.ts b/core/src/components/tabs/test/placements/e2e.ts new file mode 100644 index 0000000000..623728f073 --- /dev/null +++ b/core/src/components/tabs/test/placements/e2e.ts @@ -0,0 +1,10 @@ +import { newE2EPage } from '@stencil/core/testing'; + +it('tab-group: placements', async () => { + const page = await newE2EPage({ + url: '/src/components/tab-group/test/placements?ionic:_testing=true' + }); + + const compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); +}); diff --git a/core/src/components/tabs/test/placements/index.html b/core/src/components/tabs/test/placements/index.html index d589b5a60a..007508ff8e 100644 --- a/core/src/components/tabs/test/placements/index.html +++ b/core/src/components/tabs/test/placements/index.html @@ -14,7 +14,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -40,7 +40,7 @@ - + @@ -53,10 +53,23 @@ - + - + + + + + + + + + + + + + + diff --git a/core/src/components/tabs/test/preview/index.html b/core/src/components/tabs/test/preview/index.html index 6f222a82a6..b8c82d1cba 100644 --- a/core/src/components/tabs/test/preview/index.html +++ b/core/src/components/tabs/test/preview/index.html @@ -14,8 +14,8 @@ - + Tab One @@ -24,10 +24,9 @@ Tab One - - + @@ -40,7 +39,7 @@ - + @@ -53,8 +52,29 @@ - - + + + + + Plain List + + + + + Schedule + + + + + Stopwatch + + + + + Messages + + + - - - diff --git a/core/src/components/tabs/test/vanilla/index.html b/core/src/components/tabs/test/vanilla/index.html index fb5df04106..c1b264d567 100644 --- a/core/src/components/tabs/test/vanilla/index.html +++ b/core/src/components/tabs/test/vanilla/index.html @@ -15,13 +15,25 @@ - +
Div One
- +
Div Two
+ + + + Tab One + + + + + Tab Two + + +