feat(item-option): add inner and container parts (#30929)

Issue number: N/A

---------

## What is the current behavior?
The inner structural elements of item-option 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

## 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:
Brandy Smith
2026-02-27 13:26:01 -05:00
committed by GitHub
parent 5cdeb7fd35
commit f8f7ffda31
3 changed files with 86 additions and 2 deletions

View File

@@ -987,6 +987,8 @@ ion-item-option,css-prop,--background,ios
ion-item-option,css-prop,--background,md
ion-item-option,css-prop,--color,ios
ion-item-option,css-prop,--color,md
ion-item-option,part,container
ion-item-option,part,inner
ion-item-option,part,native
ion-item-options,none

View File

@@ -17,6 +17,8 @@ import type { Color } from '../../interface';
* @slot end - Content is placed to the right of the option text in LTR, and to the left in RTL.
*
* @part native - The native HTML button or anchor element that wraps all child elements.
* @part inner - The inner wrapper element that arranges the option content.
* @part container - The container element that wraps the start, icon-only, default, and end slots.
*/
@Component({
tag: 'ion-item-option',
@@ -109,9 +111,9 @@ export class ItemOption implements ComponentInterface, AnchorInterface, ButtonIn
})}
>
<TagType {...attrs} class="button-native" part="native" disabled={disabled}>
<span class="button-inner">
<span class="button-inner" part="inner">
<slot name="top"></slot>
<div class="horizontal-wrapper">
<div class="horizontal-wrapper" part="container">
<slot name="start"></slot>
<slot name="icon-only"></slot>
<slot></slot>

View File

@@ -0,0 +1,80 @@
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-option: custom'), () => {
test.describe('CSS shadow parts', () => {
test('should be able to customize native part', async ({ page }) => {
await page.setContent(
`
<style>
ion-item-option::part(native) {
background-color: red;
}
</style>
<ion-item-option>Option</ion-item-option>
`,
config
);
const itemOption = page.locator('ion-item-option');
const backgroundColor = await itemOption.evaluate((el) => {
const shadowRoot = el.shadowRoot;
const native = shadowRoot?.querySelector('.button-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-option::part(inner) {
background-color: green;
}
</style>
<ion-item-option>Option</ion-item-option>
`,
config
);
const itemOption = page.locator('ion-item-option');
const backgroundColor = await itemOption.evaluate((el) => {
const shadowRoot = el.shadowRoot;
const inner = shadowRoot?.querySelector('.button-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-option::part(container) {
background-color: blue;
}
</style>
<ion-item-option>Option</ion-item-option>
`,
config
);
const itemOption = page.locator('ion-item-option');
const backgroundColor = await itemOption.evaluate((el) => {
const shadowRoot = el.shadowRoot;
const container = shadowRoot?.querySelector('.horizontal-wrapper');
return container ? window.getComputedStyle(container).backgroundColor : '';
});
expect(backgroundColor).toBe('rgb(0, 0, 255)');
});
});
});
});