Merge branch 'next' into chore-sync-next-ROU-4848

This commit is contained in:
Brandy Carney
2024-05-30 10:01:13 -04:00
110 changed files with 3105 additions and 56 deletions

View File

@ -15,6 +15,7 @@ ignoreFiles:
- src/themes/functions.string.scss - src/themes/functions.string.scss
- src/themes/native.theme.default.scss - src/themes/native.theme.default.scss
- src/css/themes/*.scss - src/css/themes/*.scss
- scripts/tokens/*.css
indentation: 2 indentation: 2

View File

@ -184,6 +184,8 @@ ion-app,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-avatar,shadow ion-avatar,shadow
ion-avatar,prop,mode,"ios" | "md",undefined,false,false ion-avatar,prop,mode,"ios" | "md",undefined,false,false
ion-avatar,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false
ion-avatar,prop,size,"large" | "medium" | "small" | "xlarge" | "xsmall" | undefined,undefined,false,false
ion-avatar,prop,theme,"ios" | "md" | "ionic",undefined,false,false ion-avatar,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-avatar,css-prop,--border-radius,ionic ion-avatar,css-prop,--border-radius,ionic
ion-avatar,css-prop,--border-radius,ios ion-avatar,css-prop,--border-radius,ios
@ -310,6 +312,7 @@ ion-backdrop,event,ionBackdropTap,void,true
ion-badge,shadow ion-badge,shadow
ion-badge,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true ion-badge,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-badge,prop,mode,"ios" | "md",undefined,false,false ion-badge,prop,mode,"ios" | "md",undefined,false,false
ion-badge,prop,size,"large" | "medium" | "small" | "xlarge" | undefined,undefined,false,false
ion-badge,prop,theme,"ios" | "md" | "ionic",undefined,false,false ion-badge,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-badge,css-prop,--background,ionic ion-badge,css-prop,--background,ionic
ion-badge,css-prop,--background,ios ion-badge,css-prop,--background,ios

View File

@ -77,7 +77,7 @@
"build.css": "npm run css.sass && npm run css.minify", "build.css": "npm run css.sass && npm run css.minify",
"build.debug": "npm run clean && stencil build --debug", "build.debug": "npm run clean && stencil build --debug",
"build.docs.json": "stencil build --docs-json dist/docs.json", "build.docs.json": "stencil build --docs-json dist/docs.json",
"build.tokens": "node ./scripts/tokens/index.js && npm run lint.sass.fix && npm run prettier.tokens", "build.tokens": "node ./scripts/tokens/index.js && npm run lint.fix && npm run prettier.tokens",
"clean": "node scripts/clean.js", "clean": "node scripts/clean.js",
"css.minify": "cleancss -O2 -o ./css/ionic.bundle.css ./css/ionic.bundle.css", "css.minify": "cleancss -O2 -o ./css/ionic.bundle.css ./css/ionic.bundle.css",
"css.sass": "sass --embed-sources --style compressed src/css:./css", "css.sass": "sass --embed-sources --style compressed src/css:./css",
@ -90,7 +90,7 @@
"lint.ts.fix": "npm run eslint -- --fix", "lint.ts.fix": "npm run eslint -- --fix",
"prerender.e2e": "node scripts/testing/prerender.js", "prerender.e2e": "node scripts/testing/prerender.js",
"prettier": "prettier \"./src/**/*.{html,ts,tsx,js,jsx,scss}\"", "prettier": "prettier \"./src/**/*.{html,ts,tsx,js,jsx,scss}\"",
"prettier.tokens": "prettier \"./src/foundations/*.scss\" --write --cache", "prettier.tokens": "prettier \"./src/foundations/*.{scss, html}\" --write --cache",
"start": "npm run build.css && stencil build --dev --watch --serve", "start": "npm run build.css && stencil build --dev --watch --serve",
"test": "npm run test.spec && npm run test.e2e", "test": "npm run test.spec && npm run test.e2e",
"test.spec": "stencil test --spec --max-workers=2", "test.spec": "stencil test --spec --max-workers=2",

View File

@ -5,12 +5,15 @@
// - It is probably the most well-known and widely used Design Tokens tool. It has also been regularly maintained for a long time. // - It is probably the most well-known and widely used Design Tokens tool. It has also been regularly maintained for a long time.
// - It can easily scale to different necessities we might have in the future. // - It can easily scale to different necessities we might have in the future.
const fs = require('fs');
const path = require('path');
const StyleDictionary = require('style-dictionary'); const StyleDictionary = require('style-dictionary');
const targetPath = './src/foundations/'; const targetPath = './src/foundations/';
const { const {
variablesPrefix, variablesPrefix,
getRgbaValue,
hexToRgb, hexToRgb,
generateShadowValue, generateShadowValue,
generateFontFamilyValue, generateFontFamilyValue,
@ -41,8 +44,8 @@ StyleDictionary.registerFormat({
} else if (prop.attributes.category.match('font-family')) { } else if (prop.attributes.category.match('font-family')) {
return generateFontFamilyValue(prop); return generateFontFamilyValue(prop);
} else { } else {
// TODO(ROU-4870): prevent colors with 8 characters to be created without a rgb transformation
const rgb = hexToRgb(prop.value); const rgb = hexToRgb(prop.value);
prop.value = getRgbaValue(prop.value);
return ` --${variablesPrefix}-${prop.name}: ${prop.value};${ return ` --${variablesPrefix}-${prop.name}: ${prop.value};${
rgb ? `\n --${variablesPrefix}-${prop.name}-rgb: ${rgb.r}, ${rgb.g}, ${rgb.b};` : `` rgb ? `\n --${variablesPrefix}-${prop.name}-rgb: ${rgb.r}, ${rgb.g}, ${rgb.b};` : ``
}`; }`;
@ -137,6 +140,137 @@ StyleDictionary.registerFormat({
}, },
}); });
// Register the custom format to generate HTML
// Load the HTML template
const template = fs.readFileSync(path.join(__dirname, 'preview.template.html'), 'utf8');
StyleDictionary.registerFormat({
name: 'html/tokens',
formatter: function ({ dictionary }) {
// Function to extract numerical value from token name
const extractValue = (tokenName) => {
const match = tokenName.match(/-([0-9]+)/);
return match ? parseInt(match[1], 10) : Number.MAX_SAFE_INTEGER;
};
let colorTokens = `
<table>
<thead>
<tr>
<th>Color</th>
<th>Hex</th>
<th>Token Name</th>
</tr>
</thead>
<tbody>
`;
let fontSizeTokens = '';
let boxShadowTokens = '';
let borderSizeTokens = '';
let borderRadiusTokens = '';
let borderStyleTokens = '';
let fontWeightTokens = '';
let letterSpacingTokens = '';
let spaceTokens = '';
// Collect border-radius and space tokens for separate sorting
let borderRadiusTokenList = [];
let spaceTokenList = [];
dictionary.allProperties.forEach((token) => {
if (token.attributes.category === 'color') {
colorTokens += `
<tr>
<td><div class="color-swatch" style="background-color: ${token.value};"></div></td>
<td>${token.value}</td>
<td>${token.name}</td>
</tr>
`;
} else if (token.attributes.category === 'font-size') {
fontSizeTokens += `
<div class="font-size-token" style="font-size: ${token.value};">
${token.name} (${token.value})
</div>
`;
} else if (token.attributes.category.startsWith('Elevation')) {
const cssShadow = token.value.map(generateShadowValue).join(', ');
boxShadowTokens += `
<div class="shadow-token" style="box-shadow: ${cssShadow};">
${token.name}
</div>
`;
} else if (token.attributes.category === 'border-size' || token.attributes.category === 'border-width') {
borderSizeTokens += `
<div class="border-token" style="border-width: ${token.value};">
${token.name} (${token.value})
</div>
`;
} else if (token.attributes.category === 'border-radius') {
borderRadiusTokenList.push(token); // Collect border-radius tokens
} else if (token.attributes.category === 'border-style') {
borderStyleTokens += `
<div class="border-token" style="border: 1px ${token.value} #000;">
${token.name} (${token.value})
</div>
`;
} else if (token.attributes.category === 'font-weight') {
fontWeightTokens += `
<div class="weight-token" style="font-weight: ${token.value};">
${token.name} (${token.value})
</div>
`;
} else if (token.attributes.category === 'letter-spacing') {
// Convert % to px
const letterSpacingValue = token.value.replace('%', '') + 'px';
letterSpacingTokens += `
<div class="letter-spacing-token" style="letter-spacing: ${letterSpacingValue};">
${token.name} (${letterSpacingValue})
</div>
`;
} else if (token.attributes.category === 'space') {
spaceTokenList.push(token); // Collect space tokens
}
});
// Sort border-radius and space tokens
borderRadiusTokenList.sort((a, b) => extractValue(a.name) - extractValue(b.name));
spaceTokenList.sort((a, b) => extractValue(a.name) - extractValue(b.name));
// Generate HTML for sorted border-radius tokens
borderRadiusTokenList.forEach((token) => {
borderRadiusTokens += `
<div class="border-token" style="border-radius: ${token.value};">
${token.name} (${token.value})
</div>
`;
});
// Generate HTML for sorted space tokens
spaceTokenList.forEach((token) => {
spaceTokens += `
<div class="spacing-wrapper">
<div class="space-token" style="margin: ${token.value};">
${token.name} (${token.value})
</div>
</div>
`;
});
colorTokens += '</tbody></table>';
return template
.replace('{{colorTokens}}', colorTokens)
.replace('{{fontSizeTokens}}', fontSizeTokens)
.replace('{{boxShadowTokens}}', boxShadowTokens)
.replace('{{borderSizeTokens}}', borderSizeTokens)
.replace('{{borderRadiusTokens}}', borderRadiusTokens)
.replace('{{borderStyleTokens}}', borderStyleTokens)
.replace('{{fontWeightTokens}}', fontWeightTokens)
.replace('{{letterSpacingTokens}}', letterSpacingTokens)
.replace('{{spaceTokens}}', spaceTokens);
},
});
// Custom transform to ensure unique token names // Custom transform to ensure unique token names
StyleDictionary.registerTransform({ StyleDictionary.registerTransform({
name: 'name/cti/kebab-unique', name: 'name/cti/kebab-unique',
@ -209,6 +343,16 @@ StyleDictionary.extend({
}, },
], ],
}, },
html: {
transformGroup: 'custom',
buildPath: targetPath,
files: [
{
destination: 'tokens.preview.html',
format: 'html/tokens',
},
],
},
}, },
fileHeader: { fileHeader: {
myFileHeader: () => { myFileHeader: () => {

View File

@ -0,0 +1,63 @@
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th,
td {
padding: 10px;
border: 1px solid #ccc;
text-align: left;
}
th {
background-color: #f4f4f4;
}
thead th {
position: sticky;
top: 0;
background-color: #f4f4f4;
z-index: 1;
}
.color-swatch {
width: 50px;
height: 50px;
}
.font-size-token,
.weight-token,
.letter-spacing-token {
margin: 10px 0;
}
.border-token,
.shadow-token {
margin: 10px;
padding: 10px;
}
.border-token {
border: 1px solid #000;
}
.spacing-wrapper {
background-color: lightblue;
}
.spacing-wrapper > div {
background-color: #fff;
}
.token-wrapper:has(.spacing-wrapper) {
display: flex;
flex-direction: column;
gap: 20px;
}
hr {
background-color: #ccc;
margin: 20px 0;
}

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>Design Tokens</title>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<link rel="stylesheet" href="../../scripts/tokens/preview.styles.css" />
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Design Tokens - Preview</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-horizontal">
<h1>Color Tokens</h1>
<div class="token-wrapper">{{colorTokens}}</div>
<hr />
<h1>Font Size Tokens</h1>
<div class="token-wrapper">{{fontSizeTokens}}</div>
<hr />
<h1>Font Weight Tokens</h1>
<div class="token-wrapper">{{fontWeightTokens}}</div>
<hr />
<h1>Letter Spacing Tokens</h1>
<div class="token-wrapper">{{letterSpacingTokens}}</div>
<hr />
<h1>Box Shadow Tokens</h1>
<div class="token-wrapper">{{boxShadowTokens}}</div>
<hr />
<h1>Border Size Tokens</h1>
<div class="token-wrapper">{{borderSizeTokens}}</div>
<hr />
<h1>Border Radius Tokens</h1>
<div class="token-wrapper">{{borderRadiusTokens}}</div>
<hr />
<h1>Border Style Tokens</h1>
<div class="token-wrapper">{{borderStyleTokens}}</div>
<hr />
<h1>Space Tokens</h1>
<div class="token-wrapper">{{spaceTokens}}</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -1,6 +1,18 @@
const variablesPrefix = 'ionic'; // Variable that holds the prefix used on all css and scss variables generated const variablesPrefix = 'ionic'; // Variable that holds the prefix used on all css and scss variables generated
// Generates translate an hex color value to rgb // Generates a valid rgba() color
function getRgbaValue(propValue) {
// Check if its rgba color
const isRgba = hexToRgba(propValue);
// If it is, then compose rgba() color, otherwise use the normal color
if (isRgba !== null) {
return (propValue = `rgba(${isRgba.r}, ${isRgba.g}, ${isRgba.b},${isRgba.a})`);
} else {
return propValue;
}
}
// Translates an hex color value to rgb
function hexToRgb(hex) { function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result return result
@ -12,9 +24,24 @@ function hexToRgb(hex) {
: null; : null;
} }
// Translates an hex color value to rgba
function hexToRgba(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
a: Math.round((parseInt(result[4], 16) * 100) / 255) / 100,
}
: null;
}
// Generates a valid box-shadow value from a shadow Design Token structure // Generates a valid box-shadow value from a shadow Design Token structure
function generateShadowValue(shadow) { function generateShadowValue(shadow) {
return `${shadow.offsetX} ${shadow.offsetY} ${shadow.blur} ${shadow.spread} ${shadow.color}`; const color = getRgbaValue(shadow.color);
return `${shadow.offsetX} ${shadow.offsetY} ${shadow.blur} ${shadow.spread} ${color}`;
} }
// Generates a valid font-family value from a font-family Design Token structure // Generates a valid font-family value from a font-family Design Token structure
@ -55,16 +82,19 @@ function getTypeMap(dictionary, type) {
); );
} }
// Generates a rgb color value, based on a color Design Token // Generates a final value, based if the Design Token is of type color or not
function generateRgbValue(prop) { function generateValue(prop) {
const rgb = hexToRgb(prop.value); const rgb = hexToRgb(prop.value);
let rgbDeclaration = ''; let rgbDeclaration = '';
// If the token is color, also add a rgb variable using the same color
if (rgb) { if (rgb) {
// If the token is color, also add a rgb variable using the same color
rgbDeclaration = `\n$${variablesPrefix}-${prop.name}-rgb: var(--${variablesPrefix}-${prop.name}-rgb, ${rgb.r}, ${rgb.g}, ${rgb.b});`; rgbDeclaration = `\n$${variablesPrefix}-${prop.name}-rgb: var(--${variablesPrefix}-${prop.name}-rgb, ${rgb.r}, ${rgb.g}, ${rgb.b});`;
} }
prop.value = getRgbaValue(prop.value);
return `$${variablesPrefix}-${prop.name}: var(--${variablesPrefix}-${prop.name}, ${prop.value});${rgbDeclaration}`; return `$${variablesPrefix}-${prop.name}: var(--${variablesPrefix}-${prop.name}, ${prop.value});${rgbDeclaration}`;
} }
@ -135,11 +165,13 @@ function generateSpaceUtilityClasses(prop, className) {
// Export all methods to be used on the tokens.js script // Export all methods to be used on the tokens.js script
module.exports = { module.exports = {
variablesPrefix, variablesPrefix,
getRgbaValue,
hexToRgb, hexToRgb,
hexToRgba,
generateShadowValue, generateShadowValue,
generateFontFamilyValue, generateFontFamilyValue,
generateTypographyValue, generateTypographyValue,
generateRgbValue, generateRgbValue: generateValue,
generateColorUtilityClasses, generateColorUtilityClasses,
generateFontUtilityClass, generateFontUtilityClass,
generateSpaceUtilityClasses, generateSpaceUtilityClasses,

View File

@ -335,6 +335,14 @@ 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 an avatar with slightly rounded corners, `"round"` for an avatar with fully rounded corners, or `"rectangular"` for an avatar without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
*/
"shape"?: 'soft' | 'round' | 'rectangular';
/**
* Set to `"xsmall"` for the smallest size, `"small"` for a compact size, `"medium"` for the default height and width, `"large"` for a larger size, or `"xlarge"` for the largest dimensions. Defaults to `"medium"` for the `ionic` theme, undefined for all other themes.
*/
"size"?: `xsmall` | 'small' | 'medium' | 'large' | 'xlarge';
/** /**
* The theme determines the visual appearance of the component. * The theme determines the visual appearance of the component.
*/ */
@ -409,6 +417,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 `"small"` for less height and width. Set to "medium" for slightly larger dimensions. Set to "large" for even greater height and width. Set to `"xlarge"` for the largest badge. Defaults to `"small"` for the `ionic` theme, undefined for all other themes.
*/
"size"?: 'small' | 'medium' | 'large' | 'xlarge';
/** /**
* The theme determines the visual appearance of the component. * The theme determines the visual appearance of the component.
*/ */
@ -5563,6 +5575,14 @@ 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 an avatar with slightly rounded corners, `"round"` for an avatar with fully rounded corners, or `"rectangular"` for an avatar without rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
*/
"shape"?: 'soft' | 'round' | 'rectangular';
/**
* Set to `"xsmall"` for the smallest size, `"small"` for a compact size, `"medium"` for the default height and width, `"large"` for a larger size, or `"xlarge"` for the largest dimensions. Defaults to `"medium"` for the `ionic` theme, undefined for all other themes.
*/
"size"?: `xsmall` | 'small' | 'medium' | 'large' | 'xlarge';
/** /**
* The theme determines the visual appearance of the component. * The theme determines the visual appearance of the component.
*/ */
@ -5641,6 +5661,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 `"small"` for less height and width. Set to "medium" for slightly larger dimensions. Set to "large" for even greater height and width. Set to `"xlarge"` for the largest badge. Defaults to `"small"` for the `ionic` theme, undefined for all other themes.
*/
"size"?: 'small' | 'medium' | 'large' | 'xlarge';
/** /**
* The theme determines the visual appearance of the component. * The theme determines the visual appearance of the component.
*/ */

View File

@ -0,0 +1,105 @@
@use "../../themes/ionic/ionic.globals.scss" as globals;
@import "./avatar";
// Ionic Avatar
// --------------------------------------------------
:host {
--padding-top: #{globals.$ionic-space-0};
--padding-bottom: #{globals.$ionic-space-0};
display: flex;
align-items: center;
justify-content: center;
background: globals.$ionic-color-neutral-100;
color: globals.$ionic-color-neutral-800;
font-weight: globals.$ionic-font-weight-medium;
line-height: globals.$ionic-line-height-700;
}
:host(:not(.avatar-image)) {
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
border: globals.$ionic-border-size-025 globals.$ionic-border-style-solid globals.$ionic-color-neutral-800;
}
// Avatar Sizes
// --------------------------------------------------
:host(.avatar-xsmall) {
--padding-end: #{globals.$ionic-space-050};
--padding-start: #{globals.$ionic-space-050};
width: globals.$ionic-scale-600;
height: globals.$ionic-scale-600;
font-size: globals.$ionic-font-size-300;
line-height: globals.$ionic-line-height-400;
}
:host(.avatar-small) {
--padding-end: #{globals.$ionic-space-150};
--padding-start: #{globals.$ionic-space-150};
width: globals.$ionic-scale-800;
height: globals.$ionic-scale-800;
font-size: globals.$ionic-font-size-400;
line-height: globals.$ionic-line-height-600;
}
:host(.avatar-medium) {
--padding-end: #{globals.$ionic-space-200};
--padding-start: #{globals.$ionic-space-200};
width: globals.$ionic-scale-1000;
height: globals.$ionic-scale-1000;
font-size: globals.$ionic-font-size-450;
}
:host(.avatar-large) {
--padding-end: #{globals.$ionic-space-250};
--padding-start: #{globals.$ionic-space-250};
width: globals.$ionic-scale-1200;
height: globals.$ionic-scale-1200;
font-size: globals.$ionic-font-size-500;
}
:host(.avatar-xlarge) {
--padding-end: #{globals.$ionic-space-300};
--padding-start: #{globals.$ionic-space-300};
width: globals.$ionic-scale-1400;
height: globals.$ionic-scale-1400;
font-size: globals.$ionic-font-size-550;
}
// Avatar Shapes
// --------------------------------------------------
:host(.avatar-xsmall.avatar-soft),
:host(.avatar-small.avatar-soft) {
--border-radius: #{globals.$ionic-border-radius-100};
}
:host(.avatar-soft) {
--border-radius: #{globals.$ionic-border-radius-200};
}
:host(.avatar-round) {
--border-radius: #{globals.$ionic-border-radius-full};
}
:host(.avatar-rectangular) {
--border-radius: #{globals.$ionic-border-radius-0};
}

View File

@ -1,5 +1,5 @@
import type { ComponentInterface } from '@stencil/core'; import type { ComponentInterface } from '@stencil/core';
import { Component, Host, h } from '@stencil/core'; import { Component, Element, Host, Prop, h } from '@stencil/core';
import { getIonTheme } from '../../global/ionic-global'; import { getIonTheme } from '../../global/ionic-global';
@ -12,17 +12,79 @@ import { getIonTheme } from '../../global/ionic-global';
styleUrls: { styleUrls: {
ios: 'avatar.ios.scss', ios: 'avatar.ios.scss',
md: 'avatar.md.scss', md: 'avatar.md.scss',
ionic: 'avatar.md.scss', ionic: 'avatar.ionic.scss',
}, },
shadow: true, shadow: true,
}) })
export class Avatar implements ComponentInterface { export class Avatar implements ComponentInterface {
@Element() el!: HTMLElement;
/**
* Set to `"xsmall"` for the smallest size, `"small"` for a compact size, `"medium"`
* for the default height and width, `"large"` for a larger size, or `"xlarge"` for
* the largest dimensions.
*
* Defaults to `"medium"` for the `ionic` theme, undefined for all other themes.
*/
@Prop() size?: `xsmall` | 'small' | 'medium' | 'large' | 'xlarge';
/**
* Set to `"soft"` for an avatar with slightly rounded corners,
* `"round"` for an avatar with fully rounded corners, or `"rectangular"`
* for an avatar without rounded corners.
*
* Defaults to `"round"` for the `ionic` theme, undefined for all other themes.
*/
@Prop() shape?: 'soft' | 'round' | 'rectangular';
get hasImage() {
return !!this.el.querySelector('ion-img') || !!this.el.querySelector('img');
}
private getSize(): string | undefined {
const theme = getIonTheme(this);
const { size } = this;
// TODO(ROU-10752): Remove theme check when sizes are defined for all themes.
if (theme !== 'ionic') {
return undefined;
}
if (size === undefined) {
return 'medium';
}
return size;
}
private getShape(): string | undefined {
const theme = getIonTheme(this);
const { shape } = this;
// TODO(ROU-10755): Remove theme check when shapes are defined for all themes.
if (theme !== 'ionic') {
return undefined;
}
if (shape === undefined) {
return 'round';
}
return shape;
}
render() { render() {
const theme = getIonTheme(this); const theme = getIonTheme(this);
const size = this.getSize();
const shape = this.getShape();
return ( return (
<Host <Host
class={{ class={{
[theme]: true, [theme]: true,
[`avatar-${size}`]: size !== undefined,
[`avatar-${shape}`]: shape !== undefined,
[`avatar-image`]: this.hasImage,
}} }}
> >
<slot></slot> <slot></slot>

View File

@ -0,0 +1,185 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across directions.
*/
configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ config, screenshot, title }) => {
test.describe(title('avatar: shape'), () => {
test.describe('round', () => {
test('should not have visual regressions when containing text', async ({ page }) => {
await page.setContent(
`
<ion-avatar shape="round">AB</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-shape-round-text`));
});
test('should not have visual regressions when containing an icon', async ({ page }) => {
await page.setContent(
`
<ion-avatar shape="round">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-shape-round-icon`));
});
test('should not have visual regressions when containing an image', async ({ page }) => {
await page.setContent(
`
<ion-avatar shape="round">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-shape-round-image`));
});
});
test.describe('rectangular', () => {
test('should not have visual regressions when containing text', async ({ page }) => {
await page.setContent(
`
<ion-avatar shape="rectangular">AB</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-shape-rectangular-text`));
});
test('should not have visual regressions when containing an icon', async ({ page }) => {
await page.setContent(
`
<ion-avatar shape="rectangular">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-shape-rectangular-icon`));
});
test('should not have visual regressions when containing an image', async ({ page }) => {
await page.setContent(
`
<ion-avatar shape="rectangular">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-shape-rectangular-image`));
});
});
test.describe('soft', () => {
test('should not have visual regressions when containing text', async ({ page }) => {
await page.setContent(
`
<style>
#container {
display: flex;
gap: 10px;
}
</style>
<div id="container">
<ion-avatar shape="soft" size="xsmall">AB</ion-avatar>
<ion-avatar shape="soft" size="small">AB</ion-avatar>
<ion-avatar shape="soft">AB</ion-avatar>
</div>
`,
config
);
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`avatar-shape-soft-text`));
});
test('should not have visual regressions when containing an icon', async ({ page }) => {
await page.setContent(
`
<style>
#container {
display: flex;
gap: 10px;
}
</style>
<div id="container">
<ion-avatar shape="soft" size="xsmall">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
<ion-avatar shape="soft" size="small">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
<ion-avatar shape="soft">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
</div>
`,
config
);
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`avatar-shape-soft-icon`));
});
test('should not have visual regressions when containing an image', async ({ page }) => {
await page.setContent(
`
<style>
#container {
display: flex;
gap: 10px;
}
</style>
<div id="container">
<ion-avatar shape="soft" size="xsmall">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
<ion-avatar shape="soft" size="small">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
<ion-avatar shape="soft">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
</div>
`,
config
);
const container = page.locator('#container');
await expect(container).toHaveScreenshot(screenshot(`avatar-shape-soft-image`));
});
});
});
});

View File

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Avatar - 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>
.container {
display: flex;
gap: 10px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Avatar - Shape</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding" id="content" no-bounce>
<h2>Default</h2>
<div class="container">
<ion-avatar>AB</ion-avatar>
<ion-avatar>
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
<ion-avatar>
<img src="/src/components/avatar/test/avatar.svg" />
</ion-avatar>
</div>
<h2>Soft</h2>
<div class="container">
<ion-avatar shape="soft" size="xsmall">AB</ion-avatar>
<ion-avatar shape="soft" size="small">AB</ion-avatar>
<ion-avatar shape="soft" size="medium">AB</ion-avatar>
<ion-avatar shape="soft" size="large">AB</ion-avatar>
<ion-avatar shape="soft" size="xlarge">AB</ion-avatar>
</div>
<h2>Round</h2>
<div class="container">
<ion-avatar shape="round" size="xsmall">AB</ion-avatar>
<ion-avatar shape="round" size="small">AB</ion-avatar>
<ion-avatar shape="round" size="medium">AB</ion-avatar>
<ion-avatar shape="round" size="large">AB</ion-avatar>
<ion-avatar shape="round" size="xlarge">AB</ion-avatar>
</div>
<h2>Rectangular</h2>
<div class="container">
<ion-avatar shape="rectangular" size="xsmall">AB</ion-avatar>
<ion-avatar shape="rectangular" size="small">AB</ion-avatar>
<ion-avatar shape="rectangular" size="medium">AB</ion-avatar>
<ion-avatar shape="rectangular" size="large">AB</ion-avatar>
<ion-avatar shape="rectangular" size="xlarge">AB</ion-avatar>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -0,0 +1,234 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across directions.
*/
configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ config, screenshot, title }) => {
test.describe(title('avatar: size'), () => {
test.describe('xsmall', () => {
test('should not have visual regressions when containing text', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="xsmall">AB</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-xsmall-text`));
});
test('should not have visual regressions when containing an icon', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="xsmall">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-xsmall-icon`));
});
test('should not have visual regressions when containing an image', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="xsmall">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-xsmall-image`));
});
});
test.describe('small', () => {
test('should not have visual regressions when containing text', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="small">AB</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-small-text`));
});
test('should not have visual regressions when containing an icon', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="small">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-small-icon`));
});
test('should not have visual regressions when containing an image', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="small">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-small-image`));
});
});
test.describe('medium', () => {
test('should not have visual regressions when containing text', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="medium">AB</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-medium-text`));
});
test('should not have visual regressions when containing an icon', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="medium">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-medium-icon`));
});
test('should not have visual regressions when containing an image', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="medium">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-medium-image`));
});
});
test.describe('large', () => {
test('should not have visual regressions when containing text', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="large">AB</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-large-text`));
});
test('should not have visual regressions when containing an icon', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="large">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-large-icon`));
});
test('should not have visual regressions when containing an image', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="large">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-large-image`));
});
});
test.describe('xlarge', () => {
test('should not have visual regressions when containing text', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="xlarge">AB</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-xlarge-text`));
});
test('should not have visual regressions when containing an icon', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="xlarge">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-xlarge-icon`));
});
test('should not have visual regressions when containing an image', async ({ page }) => {
await page.setContent(
`
<ion-avatar size="xlarge">
<img src="/src/components/avatar/test/avatar.svg"/>
</ion-avatar>
`,
config
);
const avatar = page.locator('ion-avatar');
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-xlarge-image`));
});
});
});
});

View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Avatar - Size</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>
.container {
display: flex;
gap: 10px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Avatar - Size</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding" id="content" no-bounce>
<h2>Default</h2>
<div class="container">
<ion-avatar>AB</ion-avatar>
<ion-avatar>
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
<ion-avatar>
<img src="/src/components/avatar/test/avatar.svg" />
</ion-avatar>
</div>
<h2>Text</h2>
<div class="container">
<ion-avatar size="xsmall">AB</ion-avatar>
<ion-avatar size="small">AB</ion-avatar>
<ion-avatar size="medium">AB</ion-avatar>
<ion-avatar size="large">AB</ion-avatar>
<ion-avatar size="xlarge">AB</ion-avatar>
</div>
<h2>Icons</h2>
<div class="container">
<ion-avatar size="xsmall">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
<ion-avatar size="small">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
<ion-avatar size="medium">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
<ion-avatar size="large">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
<ion-avatar size="xlarge">
<ion-icon name="person-outline"></ion-icon>
</ion-avatar>
</div>
<h2>Images</h2>
<div class="container">
<ion-avatar size="xsmall">
<img src="/src/components/avatar/test/avatar.svg" />
</ion-avatar>
<ion-avatar size="small">
<img src="/src/components/avatar/test/avatar.svg" />
</ion-avatar>
<ion-avatar size="medium">
<img src="/src/components/avatar/test/avatar.svg" />
</ion-avatar>
<ion-avatar size="large">
<img src="/src/components/avatar/test/avatar.svg" />
</ion-avatar>
<ion-avatar size="xlarge">
<img src="/src/components/avatar/test/avatar.svg" />
</ion-avatar>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -0,0 +1,68 @@
@use "../../themes/ionic/ionic.globals.scss" as globals;
@import "./badge";
// Ionic Badge
// --------------------------------------------------
:host {
--padding-start: #{globals.$ionic-space-100};
--padding-end: #{globals.$ionic-space-100};
--padding-top: #{globals.$ionic-space-0};
--padding-bottom: #{globals.$ionic-space-0};
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: globals.$ionic-font-weight-medium;
}
// Badge Sizes
// --------------------------------------------------
/* Small Badge */
:host(.badge-small) {
min-width: globals.$ionic-scale-800;
height: globals.$ionic-scale-800;
font-size: globals.$ionic-font-size-400;
line-height: globals.$ionic-line-height-600;
}
/* Medium Badge */
:host(.badge-medium) {
min-width: globals.$ionic-scale-1000;
height: globals.$ionic-scale-1000;
font-size: globals.$ionic-font-size-450;
line-height: globals.$ionic-line-height-700;
}
/* Large Badge */
:host(.badge-large) {
--padding-start: #{globals.$ionic-space-200};
--padding-end: #{globals.$ionic-space-200};
min-width: globals.$ionic-scale-1200;
height: globals.$ionic-scale-1200;
font-size: globals.$ionic-font-size-500;
line-height: globals.$ionic-line-height-700;
}
/* Extra Large Badge */
:host(.badge-xlarge) {
--padding-start: #{globals.$ionic-space-200};
--padding-end: #{globals.$ionic-space-200};
min-width: globals.$ionic-scale-1400;
height: globals.$ionic-scale-1400;
font-size: globals.$ionic-font-size-550;
line-height: globals.$ionic-line-height-700;
}

View File

@ -14,7 +14,7 @@ import type { Color } from '../../interface';
styleUrls: { styleUrls: {
ios: 'badge.ios.scss', ios: 'badge.ios.scss',
md: 'badge.md.scss', md: 'badge.md.scss',
ionic: 'badge.md.scss', ionic: 'badge.ionic.scss',
}, },
shadow: true, shadow: true,
}) })
@ -26,12 +26,36 @@ export class Badge implements ComponentInterface {
*/ */
@Prop({ reflect: true }) color?: Color; @Prop({ reflect: true }) color?: Color;
/**
* Set to `"small"` for less height and width. Set to "medium" for slightly larger dimensions. Set to "large" for even greater height and width. Set to `"xlarge"` for the largest badge.
* Defaults to `"small"` for the `ionic` theme, undefined for all other themes.
*/
@Prop() size?: 'small' | 'medium' | 'large' | 'xlarge';
private getSize(): string | undefined {
const theme = getIonTheme(this);
const { size } = this;
// TODO(ROU-10747): Remove theme check when sizes are defined for all themes.
if (theme !== 'ionic') {
return undefined;
}
if (size === undefined) {
return 'small';
}
return size;
}
render() { render() {
const size = this.getSize();
const theme = getIonTheme(this); const theme = getIonTheme(this);
return ( return (
<Host <Host
class={createColorClasses(this.color, { class={createColorClasses(this.color, {
[theme]: true, [theme]: true,
[`badge-${size}`]: size !== undefined,
})} })}
> >
<slot></slot> <slot></slot>

View File

@ -0,0 +1,61 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across directions.
*/
configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ config, screenshot, title }) => {
test.describe(title('badge: size'), () => {
test('should render small badges', async ({ page }) => {
await page.setContent(
`
<ion-badge size="small">00</ion-badge>
`,
config
);
const badge = page.locator('ion-badge');
await expect(badge).toHaveScreenshot(screenshot(`badge-size-small`));
});
test('should render medium badges', async ({ page }) => {
await page.setContent(
`
<ion-badge size="medium">00</ion-badge>
`,
config
);
const badge = page.locator('ion-badge');
await expect(badge).toHaveScreenshot(screenshot(`badge-size-medium`));
});
test('should render large badges', async ({ page }) => {
await page.setContent(
`
<ion-badge size="large">00</ion-badge>
`,
config
);
const badge = page.locator('ion-badge');
await expect(badge).toHaveScreenshot(screenshot(`badge-size-large`));
});
test('should render xlarge badges', async ({ page }) => {
await page.setContent(
`
<ion-badge size="xlarge">00</ion-badge>
`,
config
);
const badge = page.locator('ion-badge');
await expect(badge).toHaveScreenshot(screenshot(`badge-size-xlarge`));
});
});
});

Some files were not shown because too many files have changed in this diff Show More