mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(item): add inner and container parts (#30927)
Issue number: N/A --------- ## What is the current behavior? The inner structural elements of item are not exposed as shadow parts, preventing users from being able to customize their styles directly. ## What is the new behavior? - Exposes `inner` and `container` shadow parts - Adds e2e test coverage for customizing the shadow parts - Removes css variables test, combining it with the shadow parts test ## Does this introduce a breaking change? - [ ] Yes - [x] No --------- Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
@@ -939,7 +939,9 @@ ion-item,css-prop,--ripple-color,ios
|
||||
ion-item,css-prop,--ripple-color,md
|
||||
ion-item,css-prop,--transition,ios
|
||||
ion-item,css-prop,--transition,md
|
||||
ion-item,part,container
|
||||
ion-item,part,detail-icon
|
||||
ion-item,part,inner
|
||||
ion-item,part,native
|
||||
|
||||
ion-item-divider,shadow
|
||||
|
||||
@@ -18,6 +18,8 @@ import type { RouterDirection } from '../router/utils/interface';
|
||||
* @slot end - Content is placed to the right of the item text in LTR, and to the left in RTL.
|
||||
*
|
||||
* @part native - The native HTML button, anchor or div element that wraps all child elements.
|
||||
* @part inner - The inner wrapper element that arranges the item content.
|
||||
* @part container - The wrapper element that contains the default slot.
|
||||
* @part detail-icon - The chevron icon for the item. Only applies when `detail="true"`.
|
||||
*/
|
||||
@Component({
|
||||
@@ -390,8 +392,8 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
|
||||
{...clickFn}
|
||||
>
|
||||
<slot name="start" onSlotchange={this.updateInteractivityOnSlotChange}></slot>
|
||||
<div class="item-inner">
|
||||
<div class="input-wrapper">
|
||||
<div class="item-inner" part="inner">
|
||||
<div class="input-wrapper" part="container">
|
||||
<slot onSlotchange={this.updateInteractivityOnSlotChange}></slot>
|
||||
</div>
|
||||
<slot name="end" onSlotchange={this.updateInteractivityOnSlotChange}></slot>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Item - CSS Variables</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<style>
|
||||
ion-item {
|
||||
--padding-top: 20px;
|
||||
--background: #eee;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Item CSS variables</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-vertical">
|
||||
<ion-list class="basic">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,17 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('item: CSS variables'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/item/test/css-variables`, config);
|
||||
|
||||
await page.setIonViewport();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`item-css-vars-diff`));
|
||||
});
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.7 KiB |
174
core/src/components/item/test/custom/item.e2e.ts
Normal file
174
core/src/components/item/test/custom/item.e2e.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions
|
||||
*/
|
||||
configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('item: custom'), () => {
|
||||
test.describe('CSS shadow parts', () => {
|
||||
test('should be able to customize native part', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-item::part(native) {
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item</ion-label>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const item = page.locator('ion-item');
|
||||
const backgroundColor = await item.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const native = shadowRoot?.querySelector('.item-native');
|
||||
return native ? window.getComputedStyle(native).backgroundColor : '';
|
||||
});
|
||||
expect(backgroundColor).toBe('rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
test('should be able to customize inner part', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-item::part(inner) {
|
||||
background-color: green;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item</ion-label>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const item = page.locator('ion-item');
|
||||
const backgroundColor = await item.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const inner = shadowRoot?.querySelector('.item-inner');
|
||||
return inner ? window.getComputedStyle(inner).backgroundColor : '';
|
||||
});
|
||||
expect(backgroundColor).toBe('rgb(0, 128, 0)');
|
||||
});
|
||||
|
||||
test('should be able to customize container part', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-item::part(container) {
|
||||
background-color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item</ion-label>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const item = page.locator('ion-item');
|
||||
const backgroundColor = await item.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const container = shadowRoot?.querySelector('.input-wrapper');
|
||||
return container ? window.getComputedStyle(container).backgroundColor : '';
|
||||
});
|
||||
expect(backgroundColor).toBe('rgb(0, 0, 255)');
|
||||
});
|
||||
|
||||
test('should be able to customize detail-icon part', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-item::part(detail-icon) {
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-item detail="true">
|
||||
<ion-label>Item</ion-label>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const item = page.locator('ion-item');
|
||||
const backgroundColor = await item.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const detailIcon = shadowRoot?.querySelector('.item-detail-icon');
|
||||
return detailIcon ? window.getComputedStyle(detailIcon).backgroundColor : '';
|
||||
});
|
||||
expect(backgroundColor).toBe('rgb(255, 0, 0)');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('CSS variables', () => {
|
||||
test('should be able to customize background using css variables', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-item {
|
||||
--background: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item</ion-label>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const item = page.locator('ion-item');
|
||||
const backgroundColor = await item.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const native = shadowRoot?.querySelector('.item-native');
|
||||
return native ? window.getComputedStyle(native).backgroundColor : '';
|
||||
});
|
||||
expect(backgroundColor).toBe('rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
test('should be able to customize padding using css variables', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-item {
|
||||
--padding-top: 20px;
|
||||
--padding-bottom: 20px;
|
||||
--padding-start: 10px;
|
||||
--padding-end: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item</ion-label>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const item = page.locator('ion-item');
|
||||
const paddingValues = await item.evaluate((el) => {
|
||||
const shadowRoot = el.shadowRoot;
|
||||
const native = shadowRoot?.querySelector('.item-native');
|
||||
return {
|
||||
paddingTop: native ? window.getComputedStyle(native).paddingTop : '',
|
||||
paddingBottom: native ? window.getComputedStyle(native).paddingBottom : '',
|
||||
paddingStart: native ? window.getComputedStyle(native).paddingLeft : '',
|
||||
paddingEnd: native ? window.getComputedStyle(native).paddingRight : '',
|
||||
};
|
||||
});
|
||||
expect(paddingValues.paddingTop).toBe('20px');
|
||||
expect(paddingValues.paddingBottom).toBe('20px');
|
||||
expect(paddingValues.paddingStart).toBe('10px');
|
||||
expect(paddingValues.paddingEnd).toBe('10px');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user