feat(card): Add shape feature to card and style into Ionic theme (#29292)
Issue number: resolves # --------- <!-- 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 new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Adds the new shapes to ionic theme card ## 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/.github/CONTRIBUTING.md#footer for more information. --> --------- Co-authored-by: Brandy Carney <brandy@ionic.io>
@@ -282,6 +282,7 @@ ion-card,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-card,prop,rel,string | undefined,undefined,false,false
|
||||
ion-card,prop,routerAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-card,prop,routerDirection,"back" | "forward" | "root",'forward',false,false
|
||||
ion-card,prop,shape,"rectangular" | "round" | undefined,undefined,false,true
|
||||
ion-card,prop,target,string | undefined,undefined,false,false
|
||||
ion-card,prop,theme,"ios" | "md" | "ionic",undefined,false,false
|
||||
ion-card,prop,type,"button" | "reset" | "submit",'button',false,false
|
||||
|
||||
8
core/src/components.d.ts
vendored
@@ -618,6 +618,10 @@ export namespace Components {
|
||||
* When using a router, it specifies the transition direction when navigating to another page using `href`.
|
||||
*/
|
||||
"routerDirection": RouterDirection;
|
||||
/**
|
||||
* Set to `"round"` for a card with more rounded corners, or `"rectangular"` for a card without rounded corners.
|
||||
*/
|
||||
"shape"?: 'round' | 'rectangular';
|
||||
/**
|
||||
* Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`.
|
||||
*/
|
||||
@@ -5842,6 +5846,10 @@ declare namespace LocalJSX {
|
||||
* When using a router, it specifies the transition direction when navigating to another page using `href`.
|
||||
*/
|
||||
"routerDirection"?: RouterDirection;
|
||||
/**
|
||||
* Set to `"round"` for a card with more rounded corners, or `"rectangular"` for a card without rounded corners.
|
||||
*/
|
||||
"shape"?: 'round' | 'rectangular';
|
||||
/**
|
||||
* Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`.
|
||||
*/
|
||||
|
||||
28
core/src/components/card/card.ionic.scss
Executable file
@@ -0,0 +1,28 @@
|
||||
@use "../../foundations/ionic.vars.scss" as tokens;
|
||||
@use "../../themes/ionic.mixins" as mixins;
|
||||
|
||||
// Ionic Card
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
--background: #{tokens.$ionic-color-neutral-0};
|
||||
--border-radius: #{tokens.$ionic-border-radius-rounded-small};
|
||||
|
||||
@include mixins.padding(tokens.$ionic-space-base);
|
||||
@include mixins.border-radius(var(--border-radius));
|
||||
|
||||
display: block;
|
||||
|
||||
border: #{tokens.$ionic-border-size-small} solid #{tokens.$ionic-color-neutral-50};
|
||||
|
||||
background: var(--background);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
:host(.card-round) {
|
||||
--border-radius: #{tokens.$ionic-border-radius-rounded-large};
|
||||
}
|
||||
|
||||
:host(.card-rectangular) {
|
||||
--border-radius: #{tokens.$ionic-border-radius-square};
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import type { RouterDirection } from '../router/utils/interface';
|
||||
styleUrls: {
|
||||
ios: 'card.ios.scss',
|
||||
md: 'card.md.scss',
|
||||
ionic: 'card.md.scss',
|
||||
ionic: 'card.ionic.scss',
|
||||
},
|
||||
shadow: true,
|
||||
})
|
||||
@@ -82,6 +82,11 @@ export class Card implements ComponentInterface, AnchorInterface, ButtonInterfac
|
||||
*/
|
||||
@Prop() routerAnimation: AnimationBuilder | undefined;
|
||||
|
||||
/**
|
||||
* Set to `"round"` for a card with more rounded corners, or `"rectangular"` for a card without rounded corners.
|
||||
*/
|
||||
@Prop({ reflect: true }) shape?: 'round' | 'rectangular';
|
||||
|
||||
/**
|
||||
* Specifies where to display the linked URL.
|
||||
* Only applies when an `href` is provided.
|
||||
@@ -131,12 +136,16 @@ export class Card implements ComponentInterface, AnchorInterface, ButtonInterfac
|
||||
}
|
||||
|
||||
render() {
|
||||
const { shape } = this;
|
||||
|
||||
const theme = getIonTheme(this);
|
||||
|
||||
return (
|
||||
<Host
|
||||
class={createColorClasses(this.color, {
|
||||
[theme]: true,
|
||||
// TODO(FW-6119): remove theme === 'ionic' when support for other themes is added
|
||||
[`card-${shape}`]: theme === 'ionic' && shape !== undefined,
|
||||
'card-disabled': this.disabled,
|
||||
'ion-activatable': this.isClickable(),
|
||||
})}
|
||||
|
||||
40
core/src/components/card/test/shape/card.e2e.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* Shape is only available in the Ionic theme.
|
||||
* TODO(FW-6119): add the `ios` and `md` modes when shape support is added.
|
||||
*/
|
||||
configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('card: shape'), () => {
|
||||
test.describe('default', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/card/test/shape`, config);
|
||||
|
||||
const container = page.locator('#default');
|
||||
|
||||
await expect(container).toHaveScreenshot(screenshot(`card-default`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('round', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/card/test/shape`, config);
|
||||
|
||||
const container = page.locator('#round');
|
||||
|
||||
await expect(container).toHaveScreenshot(screenshot(`card-round`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('rectangular', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/card/test/shape`, config);
|
||||
|
||||
const container = page.locator('#rectangular');
|
||||
|
||||
await expect(container).toHaveScreenshot(screenshot(`card-rectangular`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
85
core/src/components/card/test/shape/index.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Card - 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>
|
||||
|
||||
<!-- Styles only for testing purposes to identify the card shape -->
|
||||
<style>
|
||||
:root {
|
||||
--ion-background-color: #dde2ef;
|
||||
}
|
||||
|
||||
#default,
|
||||
#round,
|
||||
#rectangular {
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Card - Shape</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding ion-text-center" id="content" no-bounce>
|
||||
<h1>Default</h1>
|
||||
<div id="default" class="ion-margin-top">
|
||||
<ion-card>
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>Card Subtitle</ion-card-subtitle>
|
||||
<ion-card-title>Card Title</ion-card-title>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content>
|
||||
Keep close to Nature's heart... and break clear away, once in awhile, and climb a mountain or spend a week
|
||||
in the woods. Wash your spirit clean.
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</div>
|
||||
<h1>Round</h1>
|
||||
<div id="round" class="ion-margin-top">
|
||||
<ion-card shape="round">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>Card Subtitle</ion-card-subtitle>
|
||||
<ion-card-title>Card Title</ion-card-title>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content>
|
||||
Keep close to Nature's heart... and break clear away, once in awhile, and climb a mountain or spend a week
|
||||
in the woods. Wash your spirit clean.
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</div>
|
||||
|
||||
<h1>Rectangular</h1>
|
||||
<div id="rectangular" class="ion-margin-top">
|
||||
<ion-card shape="rectangular">
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>Card Subtitle</ion-card-subtitle>
|
||||
<ion-card-title>Card Title</ion-card-title>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content>
|
||||
Keep close to Nature's heart... and break clear away, once in awhile, and climb a mountain or spend a week
|
||||
in the woods. Wash your spirit clean.
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
78
core/src/components/card/test/theme-ionic/index.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<!-- TODO: FW-6077 This file can be removed once the additional test cases are added -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Card - Theme Ionic</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-margin-top {
|
||||
margin-top: 32px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Card - Ionic</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding ion-text-center" id="content" no-bounce>
|
||||
<h4>Preview options</h4>
|
||||
|
||||
<p>
|
||||
<ion-select id="select-shape" justify="space-between" interface="alert" label="Shape" placeholder="">
|
||||
<ion-select-option value="">default</ion-select-option>
|
||||
<ion-select-option value="round">Round</ion-select-option>
|
||||
<ion-select-option value="rectangular">Rectangular</ion-select-option>
|
||||
</ion-select>
|
||||
</p>
|
||||
|
||||
<div id="screenshot-wrapper" class="ion-margin-top">
|
||||
<ion-card>
|
||||
<ion-card-header>
|
||||
<ion-card-subtitle>Card Subtitle</ion-card-subtitle>
|
||||
<ion-card-title>Card Title</ion-card-title>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content>
|
||||
Keep close to Nature's heart... and break clear away, once in awhile, and climb a mountain or spend a week
|
||||
in the woods. Wash your spirit clean.
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const listOfCards = Array.from(document.querySelectorAll('ion-card'));
|
||||
|
||||
function updateAttr(el, name, value) {
|
||||
if (value === '' || value === undefined) {
|
||||
el.removeAttribute(name);
|
||||
} else {
|
||||
el.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
const selectShape = document.getElementById('select-shape');
|
||||
selectShape.addEventListener('ionChange', (e) => {
|
||||
for (const el of listOfCards) {
|
||||
updateAttr(el, 'shape', e.detail.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -43,6 +43,9 @@
|
||||
"$value": "#303d60"
|
||||
},
|
||||
"neutral": {
|
||||
"0": {
|
||||
"$value": "#ffffff"
|
||||
},
|
||||
"10": {
|
||||
"$value": "#f5f5f5"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
--ionic-color-primary-900: #010408;
|
||||
--ionic-color-primary: #1068eb;
|
||||
--ionic-color-secondary: #303d60;
|
||||
--ionic-color-neutral-0: #ffffff;
|
||||
--ionic-color-neutral-10: #f5f5f5;
|
||||
--ionic-color-neutral-50: #e7e7e7;
|
||||
--ionic-color-neutral-100: #dadada;
|
||||
|
||||
@@ -84,6 +84,12 @@
|
||||
.ionic-background-color-secondary {
|
||||
background-color: $ionic-color-secondary;
|
||||
}
|
||||
.ionic-color-neutral-0 {
|
||||
color: $ionic-color-neutral-0;
|
||||
}
|
||||
.ionic-background-color-neutral-0 {
|
||||
background-color: $ionic-color-neutral-0;
|
||||
}
|
||||
.ionic-color-neutral-10 {
|
||||
color: $ionic-color-neutral-10;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ $ionic-color-primary-800: var(--ionic-color-primary-800, #061935);
|
||||
$ionic-color-primary-900: var(--ionic-color-primary-900, #010408);
|
||||
$ionic-color-primary: var(--ionic-color-primary, #1068eb);
|
||||
$ionic-color-secondary: var(--ionic-color-secondary, #303d60);
|
||||
$ionic-color-neutral-0: var(--ionic-color-neutral-0, #ffffff);
|
||||
$ionic-color-neutral-10: var(--ionic-color-neutral-10, #f5f5f5);
|
||||
$ionic-color-neutral-50: var(--ionic-color-neutral-50, #e7e7e7);
|
||||
$ionic-color-neutral-100: var(--ionic-color-neutral-100, #dadada);
|
||||
|
||||
@@ -399,14 +399,14 @@ export declare interface IonButtons extends Components.IonButtons {}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'target', 'theme', 'type']
|
||||
inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'theme', 'type']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-card',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'target', 'theme', 'type'],
|
||||
inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'theme', 'type'],
|
||||
})
|
||||
export class IonCard {
|
||||
protected el: HTMLElement;
|
||||
|
||||
@@ -493,14 +493,14 @@ export declare interface IonButtons extends Components.IonButtons {}
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineIonCard,
|
||||
inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'target', 'theme', 'type']
|
||||
inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'theme', 'type']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-card',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'target', 'theme', 'type'],
|
||||
inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'theme', 'type'],
|
||||
standalone: true
|
||||
})
|
||||
export class IonCard {
|
||||
|
||||
@@ -184,6 +184,7 @@ export const IonCard = /*@__PURE__*/ defineContainer<JSX.IonCard>('ion-card', de
|
||||
'rel',
|
||||
'routerDirection',
|
||||
'routerAnimation',
|
||||
'shape',
|
||||
'target'
|
||||
]);
|
||||
|
||||
|
||||