feat(list): add shapes (#29622)

This commit is contained in:
Maria Hutt
2024-06-20 10:46:12 -07:00
committed by GitHub
parent a3f486bdbf
commit 3c7a00e57d
30 changed files with 579 additions and 10 deletions

View File

@ -1146,6 +1146,7 @@ ion-list,none
ion-list,prop,inset,boolean,false,false,false ion-list,prop,inset,boolean,false,false,false
ion-list,prop,lines,"full" | "inset" | "none" | undefined,undefined,false,false ion-list,prop,lines,"full" | "inset" | "none" | undefined,undefined,false,false
ion-list,prop,mode,"ios" | "md",undefined,false,false ion-list,prop,mode,"ios" | "md",undefined,false,false
ion-list,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false
ion-list,prop,theme,"ios" | "md" | "ionic",undefined,false,false ion-list,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-list,method,closeSlidingItems,closeSlidingItems() => Promise<boolean> ion-list,method,closeSlidingItems,closeSlidingItems() => Promise<boolean>

View File

@ -1755,6 +1755,10 @@ export namespace Components {
* The mode determines the platform behaviors of the component. * The mode determines the platform behaviors of the component.
*/ */
"mode"?: "ios" | "md"; "mode"?: "ios" | "md";
/**
* Set to `"soft"` for slightly rounded corners, `"round"` for fully rounded corners, or `"rectangular"` for no rounded corners. Defaults to `"round"` for the `ionic` theme when inset is `true` defaults to `"rectangular"` for the `ionic` theme when inset is `false`, undefined for all other themes.
*/
"shape"?: 'soft' | 'round' | 'rectangular';
/** /**
* The theme determines the visual appearance of the component. * The theme determines the visual appearance of the component.
*/ */
@ -7045,6 +7049,10 @@ declare namespace LocalJSX {
* The mode determines the platform behaviors of the component. * The mode determines the platform behaviors of the component.
*/ */
"mode"?: "ios" | "md"; "mode"?: "ios" | "md";
/**
* Set to `"soft"` for slightly rounded corners, `"round"` for fully rounded corners, or `"rectangular"` for no rounded corners. Defaults to `"round"` for the `ionic` theme when inset is `true` defaults to `"rectangular"` for the `ionic` theme when inset is `false`, undefined for all other themes.
*/
"shape"?: 'soft' | 'round' | 'rectangular';
/** /**
* The theme determines the visual appearance of the component. * The theme determines the visual appearance of the component.
*/ */

View File

@ -0,0 +1,123 @@
@use "../../themes/ionic/ionic.globals.scss" as globals;
@import "./list.ionic.vars.scss";
// Ionic List
// --------------------------------------------------
ion-list {
@include globals.margin(0);
@include globals.padding(0);
display: block;
contain: content;
list-style-type: none;
}
// Ionic Inset List
// --------------------------------------------------
.list-ionic.list-inset {
@include globals.margin(globals.$ionic-space-100);
}
// Ionic Shapes
//
// The border radius is applied to the list, excluding
// the header. The header is styled to appear as if
// it is above the list.
//
// This makes the border radius appear to be applied
// to the first element in the list (skips the header)
// and the last element in the list. These elements
// can be either `ion-item` or `ion-item-sliding`.
// --------------------------------------------------
.list-ionic:has(ion-list-header) {
/* Round */
&.list-round {
/**
* Only apply the border radius to the bottom of the list.
* The top of the list should not have a border radius because
* that would include the header.
*/
@include globals.border-radius(initial, initial, $list-ionic-border-radius-round, $list-ionic-border-radius-round);
}
// Target the first element in the list after the header
&.list-round ion-list-header + * {
/**
* Only apply the border radius to the top of the first element.
* The bottom of the first element should not have a border radius
* because that would not look connected to the rest.
*/
@include globals.border-radius($list-ionic-border-radius-round, $list-ionic-border-radius-round, initial, initial);
}
/* Soft */
&.list-soft {
/**
* Only apply the border radius to the bottom of the list.
* The top of the list should not have a border radius because
* that would include the header.
*/
@include globals.border-radius(initial, initial, $list-ionic-border-radius-soft, $list-ionic-border-radius-soft);
}
// Target the first element in the list after the header
&.list-soft ion-list-header + * {
/**
* Only apply the border radius to the top of the first element.
* The bottom of the first element should not have a border radius
* because that would not look connected to the rest.
*/
@include globals.border-radius($list-ionic-border-radius-soft, $list-ionic-border-radius-soft, initial, initial);
}
/* Rectangular */
&.list-rectangular {
/**
* Only apply the border radius to the bottom of the list.
* The top of the list should not have a border radius because
* that would include the header.
*/
@include globals.border-radius(
initial,
initial,
$list-ionic-border-radius-rectangular,
$list-ionic-border-radius-rectangular
);
}
// Target the first element in the list after the header
&.list-rectangular ion-list-header + * {
/**
* Only apply the border radius to the top of the first element.
* The bottom of the first element should not have a border radius
* because that would not look connected to the rest.
*/
@include globals.border-radius(
$list-ionic-border-radius-rectangular,
$list-ionic-border-radius-rectangular,
initial,
initial
);
}
}
.list-ionic:not(:has(ion-list-header)) {
/* Round */
&.list-round {
@include globals.border-radius($list-ionic-border-radius-round);
}
/* Soft */
&.list-soft {
@include globals.border-radius($list-ionic-border-radius-soft);
}
/* Rectangular */
&.list-rectangular {
@include globals.border-radius($list-ionic-border-radius-rectangular);
}
}

View File

@ -0,0 +1,11 @@
// Ionic List
// --------------------------------------------------
/// @prop - Round border radius of the list
$list-ionic-border-radius-round: globals.$ionic-border-radius-400;
/// @prop - Soft border radius of the list
$list-ionic-border-radius-soft: globals.$ionic-border-radius-200;
/// @prop - Rectangular border radius of the list
$list-ionic-border-radius-rectangular: globals.$ionic-border-radius-0;

View File

@ -32,8 +32,8 @@
* These selectors ensure the last item in the list * These selectors ensure the last item in the list
* has the correct border. * has the correct border.
* We need to consider the following scenarios: * We need to consider the following scenarios:
1. The last item in a list as long as it is not the only item. * 1. The last item in a list as long as it is not the only item.
2. The item in the last item-sliding in a list. * 2. The item in the last item-sliding in a list.
* Note that we do not select "ion-item-sliding ion-item:last-of-type" * Note that we do not select "ion-item-sliding ion-item:last-of-type"
* because that will cause the borders to disappear on * because that will cause the borders to disappear on
* items in an item-sliding when the item is the last * items in an item-sliding when the item is the last

View File

@ -12,7 +12,7 @@ import { getIonTheme } from '../../global/ionic-global';
styleUrls: { styleUrls: {
ios: 'list.ios.scss', ios: 'list.ios.scss',
md: 'list.md.scss', md: 'list.md.scss',
ionic: 'list.md.scss', ionic: 'list.ionic.scss',
}, },
}) })
export class List implements ComponentInterface { export class List implements ComponentInterface {
@ -28,6 +28,19 @@ export class List implements ComponentInterface {
*/ */
@Prop() inset = false; @Prop() inset = false;
/**
* Set to `"soft"` for slightly rounded corners,
* `"round"` for fully rounded corners,
* or `"rectangular"` for no rounded corners.
*
* Defaults to `"round"` for the `ionic` theme
* when inset is `true`
* defaults to `"rectangular"` for the `ionic`
* theme when inset is `false`,
* undefined for all other themes.
*/
@Prop() shape?: 'soft' | 'round' | 'rectangular';
/** /**
* If `ion-item-sliding` are used inside the list, this method closes * If `ion-item-sliding` are used inside the list, this method closes
* any open sliding item. * any open sliding item.
@ -43,9 +56,31 @@ export class List implements ComponentInterface {
return false; return false;
} }
private getShape(): string | undefined {
const theme = getIonTheme(this);
const { shape, inset } = this;
// TODO(ROU-10831): Remove theme check when shapes are defined for all themes.
if (theme !== 'ionic') {
return undefined;
}
if (shape === undefined && inset) {
return 'round';
}
if (shape === undefined) {
return 'rectangular';
}
return shape;
}
render() { render() {
const theme = getIonTheme(this); const theme = getIonTheme(this);
const shape = this.getShape();
const { lines, inset } = this; const { lines, inset } = this;
return ( return (
<Host <Host
role="list" role="list"
@ -54,10 +89,10 @@ export class List implements ComponentInterface {
// Used internally for styling // Used internally for styling
[`list-${theme}`]: true, [`list-${theme}`]: true,
'list-inset': inset, 'list-inset': inset,
[`list-lines-${lines}`]: lines !== undefined, [`list-lines-${lines}`]: lines !== undefined,
[`list-${theme}-lines-${lines}`]: lines !== undefined, [`list-${theme}-lines-${lines}`]: lines !== undefined,
[`list-${shape}`]: shape !== undefined,
}} }}
></Host> ></Host>
); );

View File

@ -2,7 +2,7 @@
<html lang="en" dir="ltr"> <html lang="en" dir="ltr">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>List - Basic</title> <title>List - Inset</title>
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"

View File

@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>List - Shape</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>
<style>
ion-content {
--background: #e5e5e5;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>List - Shape</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list inset>
<ion-list-header>
<ion-label>Default with inset</ion-label>
</ion-list-header>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
<ion-list>
<ion-list-header>
<ion-label>Default without inset</ion-label>
</ion-list-header>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
<ion-list shape="round">
<ion-list-header>
<ion-label>Round</ion-label>
</ion-list-header>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
<ion-list shape="soft">
<ion-list-header>
<ion-label>Soft</ion-label>
</ion-list-header>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
<ion-list shape="rectangular">
<ion-list-header>
<ion-label>Rectangular</ion-label>
</ion-list-header>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -0,0 +1,237 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('list: shape'), () => {
test.describe(title('shape: round'), () => {
test('should render without header', async ({ page }) => {
await page.setContent(
`
<style>
ion-content {
--background: #e5e5e5;
}
</style>
<ion-content>
<div class="wrapper" style="display: flex">
<ion-list inset="true" style="width: 100%" shape="round">
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
</div>
</ion-content>
`,
config
);
const listWrapper = page.locator('.wrapper');
await expect(listWrapper).toHaveScreenshot(screenshot(`list-shape-round-without-header`));
});
test('should render with header', async ({ page }) => {
await page.setContent(
`
<style>
ion-content {
--background: #e5e5e5;
}
</style>
<ion-content>
<div class="wrapper" style="display: flex">
<ion-list inset="true" style="width: 100%" shape="round">
<ion-list-header>
<ion-label>Header</ion-label>
</ion-list-header>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
</div>
</ion-content>
`,
config
);
const listWrapper = page.locator('.wrapper');
await expect(listWrapper).toHaveScreenshot(screenshot(`list-shape-round-with-header`));
});
});
test.describe(title('shape: soft'), () => {
test('should render without header', async ({ page }) => {
await page.setContent(
`
<style>
ion-content {
--background: #e5e5e5;
}
</style>
<ion-content>
<div class="wrapper" style="display: flex">
<ion-list inset="true" style="width: 100%" shape="soft">
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
</div>
</ion-content>
`,
config
);
const listWrapper = page.locator('.wrapper');
await expect(listWrapper).toHaveScreenshot(screenshot(`list-shape-soft-without-header`));
});
test('should render with header', async ({ page }) => {
await page.setContent(
`
<style>
ion-content {
--background: #e5e5e5;
}
</style>
<ion-content>
<div class="wrapper" style="display: flex">
<ion-list inset="true" style="width: 100%" shape="soft">
<ion-list-header>
<ion-label>Header</ion-label>
</ion-list-header>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
</div>
</ion-content>
`,
config
);
const listWrapper = page.locator('.wrapper');
await expect(listWrapper).toHaveScreenshot(screenshot(`list-shape-soft-with-header`));
});
});
test.describe(title('shape: rectangular'), () => {
test('should render without header', async ({ page }) => {
await page.setContent(
`
<style>
ion-content {
--background: #e5e5e5;
}
</style>
<ion-content>
<div class="wrapper" style="display: flex">
<ion-list inset="true" style="width: 100%" shape="rectangular">
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
</div>
</ion-content>
`,
config
);
const listWrapper = page.locator('.wrapper');
await expect(listWrapper).toHaveScreenshot(screenshot(`list-shape-rectangular-without-header`));
});
test('should render with header', async ({ page }) => {
await page.setContent(
`
<style>
ion-content {
--background: #e5e5e5;
}
</style>
<ion-content>
<div class="wrapper" style="display: flex">
<ion-list inset="true" style="width: 100%" shape="rectangular">
<ion-list-header>
<ion-label>Header</ion-label>
</ion-list-header>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
<ion-item>
<ion-label>
Title
<p>Subtitle</p>
</ion-label>
</ion-item>
</ion-list>
</div>
</ion-content>
`,
config
);
const listWrapper = page.locator('.wrapper');
await expect(listWrapper).toHaveScreenshot(screenshot(`list-shape-rectangular-with-header`));
});
});
});
});

View File

@ -1209,7 +1209,7 @@ export declare interface IonLabel extends Components.IonLabel {}
@ProxyCmp({ @ProxyCmp({
inputs: ['inset', 'lines', 'mode', 'theme'], inputs: ['inset', 'lines', 'mode', 'shape', 'theme'],
methods: ['closeSlidingItems'] methods: ['closeSlidingItems']
}) })
@Component({ @Component({
@ -1217,7 +1217,7 @@ export declare interface IonLabel extends Components.IonLabel {}
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>', template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['inset', 'lines', 'mode', 'theme'], inputs: ['inset', 'lines', 'mode', 'shape', 'theme'],
}) })
export class IonList { export class IonList {
protected el: HTMLElement; protected el: HTMLElement;

View File

@ -1185,7 +1185,7 @@ export declare interface IonLabel extends Components.IonLabel {}
@ProxyCmp({ @ProxyCmp({
defineCustomElementFn: defineIonList, defineCustomElementFn: defineIonList,
inputs: ['inset', 'lines', 'mode', 'theme'], inputs: ['inset', 'lines', 'mode', 'shape', 'theme'],
methods: ['closeSlidingItems'] methods: ['closeSlidingItems']
}) })
@Component({ @Component({
@ -1193,7 +1193,7 @@ export declare interface IonLabel extends Components.IonLabel {}
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>', template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['inset', 'lines', 'mode', 'theme'], inputs: ['inset', 'lines', 'mode', 'shape', 'theme'],
standalone: true standalone: true
}) })
export class IonList { export class IonList {

View File

@ -518,7 +518,8 @@ export const IonLabel = /*@__PURE__*/ defineContainer<JSX.IonLabel>('ion-label',
export const IonList = /*@__PURE__*/ defineContainer<JSX.IonList>('ion-list', defineIonList, [ export const IonList = /*@__PURE__*/ defineContainer<JSX.IonList>('ion-list', defineIonList, [
'lines', 'lines',
'inset' 'inset',
'shape'
]); ]);