feat(tab-button): add the shape property and styles for the ionic theme (#30057)

Issue number: internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
Tab button does not have a shape property.


## What is the new behavior?
- Adds support for the shape property in tab button.
- Adds styles for the "soft", "round" and "rectangular" shapes in the
ionic theme
- Defaults the shape to "round" for the ionic theme
- Adds an e2e test for shape with screenshots of all shapes


## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
This commit is contained in:
Pedro Lourenço
2024-12-06 11:44:58 +00:00
committed by GitHub
parent e16c633575
commit 861b4bfdca
23 changed files with 161 additions and 6 deletions

View File

@ -2213,6 +2213,7 @@ ion-tab-button,prop,layout,"icon-bottom" | "icon-end" | "icon-hide" | "icon-star
ion-tab-button,prop,mode,"ios" | "md",undefined,false,false
ion-tab-button,prop,rel,string | undefined,undefined,false,false
ion-tab-button,prop,selected,boolean,false,false,false
ion-tab-button,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false
ion-tab-button,prop,tab,string | undefined,undefined,false,false
ion-tab-button,prop,target,string | undefined,undefined,false,false
ion-tab-button,prop,theme,"ios" | "md" | "ionic",undefined,false,false

View File

@ -3504,6 +3504,10 @@ export namespace Components {
* The selected tab component
*/
"selected": boolean;
/**
* Set to `"soft"` for a tab-button with slightly rounded corners, `"round"` for a tab-button with fully rounded corners, or `"rectangular"` for a tab-button without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
*/
"shape"?: 'soft' | 'round' | 'rectangular';
/**
* 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.
*/
@ -8941,6 +8945,10 @@ declare namespace LocalJSX {
* The selected tab component
*/
"selected"?: boolean;
/**
* Set to `"soft"` for a tab-button with slightly rounded corners, `"round"` for a tab-button with fully rounded corners, or `"rectangular"` for a tab-button without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
*/
"shape"?: 'soft' | 'round' | 'rectangular';
/**
* 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.
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -8,8 +8,6 @@
--focus-ring-color: #{globals.$ion-border-focus-default};
--focus-ring-width: #{globals.$ion-border-radius-025};
@include globals.border-radius(globals.$ion-border-radius-200);
align-content: center;
min-height: globals.$ion-scale-1200;
@ -73,3 +71,18 @@
width: globals.$ion-scale-600;
height: globals.$ion-scale-600;
}
// Tab Button Shapes
// -------------------------------------------------------------------------------
:host(.tab-button-shape-soft) {
@include globals.border-radius(globals.$ion-border-radius-200);
}
:host(.tab-button-shape-round) {
@include globals.border-radius(globals.$ion-border-radius-400);
}
:host(.tab-button-shape-rectangular) {
@include globals.border-radius(globals.$ion-border-radius-0);
}

View File

@ -68,6 +68,15 @@ export class TabButton implements ComponentInterface, AnchorInterface {
*/
@Prop({ mutable: true }) selected = false;
/**
* Set to `"soft"` for a tab-button with slightly rounded corners,
* `"round"` for a tab-button with fully rounded corners, or `"rectangular"`
* for a tab-button without rounded corners.
*
* Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
*/
@Prop() shape?: 'soft' | 'round' | 'rectangular';
/**
* 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.
@ -107,6 +116,22 @@ export class TabButton implements ComponentInterface, AnchorInterface {
}
}
private getShape(): string | undefined {
const theme = getIonTheme(this);
const { shape } = this;
// TODO(ROU-11436): Remove theme check when shapes are defined for all themes.
if (theme !== 'ionic') {
return undefined;
}
if (shape === undefined) {
return 'round';
}
return shape;
}
private selectTab(ev: Event | KeyboardEvent) {
if (this.tab !== undefined) {
if (!this.disabled) {
@ -141,6 +166,7 @@ export class TabButton implements ComponentInterface, AnchorInterface {
render() {
const { disabled, hasIcon, hasLabel, href, rel, target, layout, selected, tab, inheritedAttributes } = this;
const theme = getIonTheme(this);
const shape = this.getShape();
const attrs = {
download: this.download,
href,
@ -164,6 +190,7 @@ export class TabButton implements ComponentInterface, AnchorInterface {
[`tab-layout-${layout}`]: true,
'ion-activatable': true,
'ion-selectable': true,
[`tab-button-shape-${shape}`]: shape !== undefined,
'ion-focusable': true,
}}
>

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Tab Button - Shape</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link rel="stylesheet" href="../../../../../css/ionic.bundle.css" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Tab Button - Shape</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-text> (Only available for ionic theme) </ion-text>
<ion-tab-bar>
<ion-tab-button>
<ion-label>Default Shape</ion-label>
<ion-icon aria-hidden="true" name="triangle-outline"></ion-icon>
</ion-tab-button>
<ion-tab-button id="soft-tab-button" shape="soft">
<ion-label>Soft Shape</ion-label>
<ion-icon aria-hidden="true" name="triangle-outline"></ion-icon>
</ion-tab-button>
<ion-tab-button id="round-tab-button" shape="round">
<ion-label>Round Shape</ion-label>
<ion-icon aria-hidden="true" name="triangle-outline"></ion-icon>
</ion-tab-button>
<ion-tab-button id="rect-tab-button" shape="rectangular">
<ion-label>Rectangular Shape</ion-label>
<ion-icon aria-hidden="true" name="triangle-outline"></ion-icon>
</ion-tab-button>
</ion-tab-bar>
</ion-content>
<style>
ion-tab-bar {
background: #d4d4d4;
}
</style>
</ion-app>
</body>
</html>

View File

@ -0,0 +1,51 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
import type { E2EPage, E2EPageOptions, ScreenshotFn } from '@utils/test/playwright';
class TabButtonFixture {
readonly page: E2EPage;
constructor(page: E2EPage) {
this.page = page;
}
async goto(config: E2EPageOptions) {
const { page } = this;
await page.goto(`/src/components/tab-button/test/shape`, config);
}
async screenshot(screenshotModifier: string, screenshot: ScreenshotFn, buttonId: string) {
const { page } = this;
const screenshotString = screenshot(`tab-button-${screenshotModifier}`);
const tabButton = page.locator(buttonId);
await expect(tabButton).toHaveScreenshot(screenshotString);
}
}
/**
* This behavior does not vary across directions.
*/
configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
test.describe(title('tab-button: shape'), () => {
let tabButtonFixture: TabButtonFixture;
test.beforeEach(async ({ page }) => {
tabButtonFixture = new TabButtonFixture(page);
await tabButtonFixture.goto(config);
});
test('should render a soft tab button', async () => {
await tabButtonFixture.screenshot('shape-soft', screenshot, '#soft-tab-button');
});
test('should render a round tab button', async () => {
await tabButtonFixture.screenshot('shape-round', screenshot, '#round-tab-button');
});
test('should render a rectangular tab button', async () => {
await tabButtonFixture.screenshot('shape-rectangular', screenshot, '#rect-tab-button');
});
});
});

View File

@ -2276,14 +2276,14 @@ export declare interface IonTabBar extends Components.IonTabBar {}
@ProxyCmp({
inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'tab', 'target', 'theme']
inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'shape', 'tab', 'target', 'theme']
})
@Component({
selector: 'ion-tab-button',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'tab', 'target', 'theme'],
inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'shape', 'tab', 'target', 'theme'],
})
export class IonTabButton {
protected el: HTMLElement;

View File

@ -2081,14 +2081,14 @@ export declare interface IonTabBar extends Components.IonTabBar {}
@ProxyCmp({
defineCustomElementFn: defineIonTabButton,
inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'tab', 'target', 'theme']
inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'shape', 'tab', 'target', 'theme']
})
@Component({
selector: 'ion-tab-button',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'tab', 'target', 'theme'],
inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'shape', 'tab', 'target', 'theme'],
standalone: true
})
export class IonTabButton {