From fd55153b3bc466f891623a1eb02248fee8068141 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 21 Oct 2026 15:52:33 -0700 Subject: [PATCH 01/13] feat(button): add recipe and tokens --- BREAKING.md | 81 +- core/api.txt | 307 +++++-- core/src/components.d.ts | 46 +- core/src/components/button/_button.docs.scss | 237 ++++++ core/src/components/button/button.common.scss | 314 ------- .../src/components/button/button.interface.ts | 229 ++++++ core/src/components/button/button.ionic.scss | 309 ------- core/src/components/button/button.ios.scss | 308 ------- .../components/button/button.ios.vars.scss | 120 --- core/src/components/button/button.md.scss | 262 ------ .../src/components/button/button.md.vars.scss | 98 --- core/src/components/button/button.native.scss | 59 -- core/src/components/button/button.scss | 472 +++++++++++ core/src/components/button/button.tsx | 154 ++-- .../button/test/basic/button.e2e.ts | 4 +- .../components/button/test/basic/index.html | 4 +- .../src/components/button/test/button.spec.ts | 133 ++- .../button/test/shape/button.e2e.ts | 4 +- .../components/button/test/shape/index.html | 54 ++ .../button/test/theme-ionic/index.html | 201 ----- .../src/components/buttons/buttons.ionic.scss | 11 +- core/src/components/buttons/buttons.ios.scss | 57 +- core/src/components/buttons/buttons.md.scss | 72 +- .../components/datetime/datetime.ionic.scss | 16 +- core/src/components/datetime/datetime.md.scss | 11 +- .../components/datetime/datetime.native.scss | 14 +- .../input-password-toggle.ionic.scss | 1 - core/src/components/input/input.ios.scss | 1 - core/src/components/input/input.md.scss | 1 - core/src/components/item/item.common.scss | 4 + core/src/components/item/item.ios.scss | 2 +- core/src/components/item/item.md.scss | 2 +- core/src/components/select/select.ionic.scss | 1 - core/src/components/select/select.ios.scss | 1 - core/src/components/select/select.md.scss | 1 - .../src/components/textarea/textarea.ios.scss | 1 - core/src/components/textarea/textarea.md.scss | 1 - core/src/themes/ionic/default.tokens.ts | 772 ++++++++++++++++++ core/src/themes/ios/default.tokens.ts | 759 ++++++++++++++++- core/src/themes/md/default.tokens.ts | 766 ++++++++++++++++- core/src/themes/themes.interfaces.ts | 34 +- core/src/utils/test/theme.spec.ts | 253 ++++++ core/src/utils/theme.ts | 208 ++++- .../angular/src/lazy/directives/proxies.ts | 4 +- .../src/standalone/directives/ion-button.ts | 4 +- 45 files changed, 4371 insertions(+), 2022 deletions(-) create mode 100644 core/src/components/button/_button.docs.scss delete mode 100644 core/src/components/button/button.common.scss create mode 100644 core/src/components/button/button.interface.ts delete mode 100644 core/src/components/button/button.ionic.scss delete mode 100644 core/src/components/button/button.ios.scss delete mode 100644 core/src/components/button/button.ios.vars.scss delete mode 100644 core/src/components/button/button.md.scss delete mode 100644 core/src/components/button/button.md.vars.scss delete mode 100644 core/src/components/button/button.native.scss create mode 100644 core/src/components/button/button.scss delete mode 100644 core/src/components/button/test/theme-ionic/index.html diff --git a/BREAKING.md b/BREAKING.md index f9e61176941..4c4af559ba9 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -50,7 +50,86 @@ This is a comprehensive list of the breaking changes introduced in the major ver

Button

-- The `border-radius` of the `ios` and `md` button now defaults to `6px` and `999px` instead of `14px` and `4px`, respectively, in accordance with the iOS and Material Design 3 guidelines. To revert to the previous appearance, set the `shape` to `"soft"` for `md` and override the `--border-radius` CSS variable for `ios` to `14px`, or set it to a different value entirely. +The following breaking changes apply to `ion-button`: + +1. The `border-radius` of the `ios` button is now `6px` at every size, instead of `6px`, `14px` and `16px` for the small, default and large sizes. The `md` button now defaults to `999px` instead of `4px`. Both follow the iOS and Material Design 3 guidelines. To revert to the previous appearance, set the `shape` to `"soft"` for `md`. For `ios`, set `IonButton.shape.soft.border.radius` for global styles or `--ion-button-shape-soft-border-radius` for component-specific styles to `14px`, and to `6px` and `16px` on small and large buttons. +2. Icon-only buttons in the `start` and `end` slots of `ion-input`, `ion-select` and `ion-textarea` now follow the button's `shape` instead of always being circles. For `ios`, set the `shape` to `"round"` to keep them circular. +3. The font size of `clear` buttons in the `ios` theme now matches the other fills at each size, instead of `17px` at the default size. To keep the previous size, set `--ion-button-size-medium-font-size` to `17px` on those buttons. +4. The `ios` button is now `28px`, `34px` and `50px` tall at the small, default and large sizes, with less padding, following the iOS guidelines. To revert to the previous appearance, set `IonButton.size..minHeight` for global styles or `--ion-button-size--min-height` for component-specific styles to `2.1em`, `3.1em` and `3.1em`, and the padding (`IonButton.size..padding` or `--ion-button-size--padding-*`) to `4px 0.9em`, `13px 1em` and `17px 1em`. +5. Icon-only buttons are now squares as tall as a text button of the same size. In `md`, this makes them a few pixels smaller at the small and default sizes and larger at the large size. To change their size, set the size's `minHeight` as described above. +6. CSS variables have been replaced. [6](#version-10x-button-replaced-css-variables) +7. `--padding-*` CSS variables are no longer part of the documented public API (but remain functional). [7](#version-10x-button-padding-variables) +8. The fill, size, shape and expand classes have been renamed. [8](#version-10x-button-renamed-classes) +9. The `default` size has been deprecated. [9](#version-10x-button-deprecated-default-size) +10. Theme classes (`ion-button.md`, `ion-button.ios`) are no longer supported. [10](#version-10x-button-theme-classes) + +
Replaced CSS variables
+ +Use the new token structure for global styles, or the corresponding CSS variable for component-specific overrides. Replace `` with `solid`, `outline` or `clear`, and `` with `crisp`, `soft`, `round` or `rectangular`. Buttons with a `color` read the same tokens under `semantic`, for example `IonButton.hue.bold.solid.semantic.default.background`. + +| Old (9.x) | New token (global) | New CSS variable (component-specific) | +|---|---|---| +| `--background` | `IonButton.hue.bold..default.background` | `--ion-button-hue-bold--default-background` | +| `--background-activated` | `IonButton.hue.bold..activated.background` | `--ion-button-hue-bold--activated-background` | +| `--background-focused` | `IonButton.hue.bold..focus.background` | `--ion-button-hue-bold--focus-background` | +| `--background-hover` | `IonButton.hue.bold..hover.background` | `--ion-button-hue-bold--hover-background` | +| `--border-color` | `IonButton.hue.bold..default.border.color` | `--ion-button-hue-bold--default-border-color` | +| `--border-radius` | `IonButton.shape..border.radius` | `--ion-button-shape--border-radius` | +| `--border-style` | `IonButton.hue.bold..default.border.style` | `--ion-button-hue-bold--default-border-style` | +| `--border-width` | `IonButton.hue.bold..default.border.width` | `--ion-button-hue-bold--default-border-width` | +| `--box-shadow` | `IonButton.hue.bold..default.box.shadow` | `--ion-button-hue-bold--default-box-shadow` | +| `--color` | `IonButton.hue.bold..default.color` | `--ion-button-hue-bold--default-color` | +| `--color-activated` | `IonButton.hue.bold..activated.color` | `--ion-button-hue-bold--activated-color` | +| `--color-focused` | `IonButton.hue.bold..focus.color` | `--ion-button-hue-bold--focus-color` | +| `--color-hover` | `IonButton.hue.bold..hover.color` | `--ion-button-hue-bold--hover-color` | +| `--ripple-color` | `IonButton.hue.bold..rippleEffect.color` | `--ion-button-hue-bold--ripple-effect-color` | +| `--transition` | `IonButton.transition` | `--ion-button-transition` | + +> [!NOTE] +> `--background-activated-opacity`, `--background-focused-opacity` and `--background-hover-opacity` have been removed with no one-to-one replacement. Each state's background is now a single color that includes its opacity, so set the final color, for example `color-mix(in srgb, var(--ion-color-primary) 12%, transparent)`. States are also drawn on the native element itself, so styles targeting `::part(native)::after` no longer affect them. + +> [!NOTE] +> `--opacity` has been removed with no replacement. Set `opacity` on `ion-button` instead. + +
Padding variables
+ +New code should use the token-based API. Replace `` with `small`, `medium` or `large`, and use `iconOnly.padding` under the size for icon-only buttons: + +| Old (9.x) | New token (global) | New CSS variable (component-specific) | +|---|---|---| +| `--padding-top` | `IonButton.size..padding.top` | `--ion-button-size--padding-top` | +| `--padding-end` | `IonButton.size..padding.end` | `--ion-button-size--padding-end` | +| `--padding-bottom` | `IonButton.size..padding.bottom` | `--ion-button-size--padding-bottom` | +| `--padding-start` | `IonButton.size..padding.start` | `--ion-button-size--padding-start` | + +> [!NOTE] +> The `--padding-*` overrides and the `.ion-padding`, `.ion-padding-*` utility classes in `css/padding.scss` continue to work when set on the button itself: `ion-button` honors them ahead of the new tokens. They are no longer part of the documented public API (only `--ion-button-size--padding-*` is listed in `core/api.txt`), but existing usage will not break. + +
Renamed classes
+ +Update any styles that target these classes: + +| Old (9.x) | New | +|---|---| +| `.button-solid` | `.button-fill-solid` | +| `.button-outline` | `.button-fill-outline` | +| `.button-clear` | `.button-fill-clear` | +| `.button-small` | `.button-size-small` | +| `.button-default` | `.button-size-medium` | +| `.button-large` | `.button-size-large` | +| `.button-soft` | `.button-shape-soft` | +| `.button-round` | `.button-shape-round` | +| `.button-rectangular` | `.button-shape-rectangular` | +| `.button-block` | `.button-expand-block` | +| `.button-full` | `.button-expand-full` | + +
Deprecated default size
+ +The `default` value of the `size` property is deprecated. It logs a warning and renders the same as `medium`. Use `size="medium"` instead. + +
Theme classes
+ +Remove any instances that target the theme classes: `ion-button.md`, `ion-button.ios`.

Card

diff --git a/core/api.txt b/core/api.txt index 5b4a827ab45..4236009ffa4 100644 --- a/core/api.txt +++ b/core/api.txt @@ -443,83 +443,248 @@ ion-button,prop,mode,"ios" | "md",undefined,false,false ion-button,prop,rel,string | undefined,undefined,false,false ion-button,prop,routerAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false ion-button,prop,routerDirection,"back" | "forward" | "root",'forward',false,false -ion-button,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,true +ion-button,prop,shape,"crisp" | "rectangular" | "round" | "soft" | undefined,undefined,false,true ion-button,prop,size,"default" | "large" | "medium" | "small" | undefined,undefined,false,true ion-button,prop,strong,boolean,false,false,false ion-button,prop,target,string | undefined,undefined,false,false -ion-button,prop,theme,"ios" | "md" | "ionic",undefined,false,false ion-button,prop,type,"button" | "reset" | "submit",'button',false,false ion-button,event,ionBlur,void,true ion-button,event,ionFocus,void,true -ion-button,css-prop,--background,ionic -ion-button,css-prop,--background,ios -ion-button,css-prop,--background,md -ion-button,css-prop,--background-activated,ionic -ion-button,css-prop,--background-activated,ios -ion-button,css-prop,--background-activated,md -ion-button,css-prop,--background-activated-opacity,ionic -ion-button,css-prop,--background-activated-opacity,ios -ion-button,css-prop,--background-activated-opacity,md -ion-button,css-prop,--background-focused,ionic -ion-button,css-prop,--background-focused,ios -ion-button,css-prop,--background-focused,md -ion-button,css-prop,--background-focused-opacity,ionic -ion-button,css-prop,--background-focused-opacity,ios -ion-button,css-prop,--background-focused-opacity,md -ion-button,css-prop,--background-hover,ionic -ion-button,css-prop,--background-hover,ios -ion-button,css-prop,--background-hover,md -ion-button,css-prop,--background-hover-opacity,ionic -ion-button,css-prop,--background-hover-opacity,ios -ion-button,css-prop,--background-hover-opacity,md -ion-button,css-prop,--border-color,ionic -ion-button,css-prop,--border-color,ios -ion-button,css-prop,--border-color,md -ion-button,css-prop,--border-radius,ionic -ion-button,css-prop,--border-radius,ios -ion-button,css-prop,--border-radius,md -ion-button,css-prop,--border-style,ionic -ion-button,css-prop,--border-style,ios -ion-button,css-prop,--border-style,md -ion-button,css-prop,--border-width,ionic -ion-button,css-prop,--border-width,ios -ion-button,css-prop,--border-width,md -ion-button,css-prop,--box-shadow,ionic -ion-button,css-prop,--box-shadow,ios -ion-button,css-prop,--box-shadow,md -ion-button,css-prop,--color,ionic -ion-button,css-prop,--color,ios -ion-button,css-prop,--color,md -ion-button,css-prop,--color-activated,ionic -ion-button,css-prop,--color-activated,ios -ion-button,css-prop,--color-activated,md -ion-button,css-prop,--color-focused,ionic -ion-button,css-prop,--color-focused,ios -ion-button,css-prop,--color-focused,md -ion-button,css-prop,--color-hover,ionic -ion-button,css-prop,--color-hover,ios -ion-button,css-prop,--color-hover,md -ion-button,css-prop,--opacity,ionic -ion-button,css-prop,--opacity,ios -ion-button,css-prop,--opacity,md -ion-button,css-prop,--padding-bottom,ionic -ion-button,css-prop,--padding-bottom,ios -ion-button,css-prop,--padding-bottom,md -ion-button,css-prop,--padding-end,ionic -ion-button,css-prop,--padding-end,ios -ion-button,css-prop,--padding-end,md -ion-button,css-prop,--padding-start,ionic -ion-button,css-prop,--padding-start,ios -ion-button,css-prop,--padding-start,md -ion-button,css-prop,--padding-top,ionic -ion-button,css-prop,--padding-top,ios -ion-button,css-prop,--padding-top,md -ion-button,css-prop,--ripple-color,ionic -ion-button,css-prop,--ripple-color,ios -ion-button,css-prop,--ripple-color,md -ion-button,css-prop,--transition,ionic -ion-button,css-prop,--transition,ios -ion-button,css-prop,--transition,md +ion-button,css-prop,--ion-button-font-family +ion-button,css-prop,--ion-button-hue-bold-clear-activated-background +ion-button,css-prop,--ion-button-hue-bold-clear-activated-box-shadow +ion-button,css-prop,--ion-button-hue-bold-clear-activated-color +ion-button,css-prop,--ion-button-hue-bold-clear-activated-opacity +ion-button,css-prop,--ion-button-hue-bold-clear-default-background +ion-button,css-prop,--ion-button-hue-bold-clear-default-border-color +ion-button,css-prop,--ion-button-hue-bold-clear-default-border-style +ion-button,css-prop,--ion-button-hue-bold-clear-default-border-width +ion-button,css-prop,--ion-button-hue-bold-clear-default-box-shadow +ion-button,css-prop,--ion-button-hue-bold-clear-default-color +ion-button,css-prop,--ion-button-hue-bold-clear-default-font-weight +ion-button,css-prop,--ion-button-hue-bold-clear-disabled-background +ion-button,css-prop,--ion-button-hue-bold-clear-disabled-opacity +ion-button,css-prop,--ion-button-hue-bold-clear-focus-background +ion-button,css-prop,--ion-button-hue-bold-clear-focus-color +ion-button,css-prop,--ion-button-hue-bold-clear-hover-background +ion-button,css-prop,--ion-button-hue-bold-clear-hover-color +ion-button,css-prop,--ion-button-hue-bold-clear-hover-opacity +ion-button,css-prop,--ion-button-hue-bold-clear-ripple-effect-color +ion-button,css-prop,--ion-button-hue-bold-clear-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-activated-background +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-activated-color +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-default-background +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-default-border-color +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-default-color +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-disabled-background +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-focus-background +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-focus-color +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-hover-background +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-hover-color +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-ripple-effect-color +ion-button,css-prop,--ion-button-hue-bold-clear-semantic-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-bold-clear-strong-font-weight +ion-button,css-prop,--ion-button-hue-bold-outline-activated-background +ion-button,css-prop,--ion-button-hue-bold-outline-activated-box-shadow +ion-button,css-prop,--ion-button-hue-bold-outline-activated-color +ion-button,css-prop,--ion-button-hue-bold-outline-activated-opacity +ion-button,css-prop,--ion-button-hue-bold-outline-default-background +ion-button,css-prop,--ion-button-hue-bold-outline-default-border-color +ion-button,css-prop,--ion-button-hue-bold-outline-default-border-style +ion-button,css-prop,--ion-button-hue-bold-outline-default-border-width +ion-button,css-prop,--ion-button-hue-bold-outline-default-box-shadow +ion-button,css-prop,--ion-button-hue-bold-outline-default-color +ion-button,css-prop,--ion-button-hue-bold-outline-default-font-weight +ion-button,css-prop,--ion-button-hue-bold-outline-disabled-background +ion-button,css-prop,--ion-button-hue-bold-outline-disabled-opacity +ion-button,css-prop,--ion-button-hue-bold-outline-focus-background +ion-button,css-prop,--ion-button-hue-bold-outline-focus-color +ion-button,css-prop,--ion-button-hue-bold-outline-hover-background +ion-button,css-prop,--ion-button-hue-bold-outline-hover-color +ion-button,css-prop,--ion-button-hue-bold-outline-hover-opacity +ion-button,css-prop,--ion-button-hue-bold-outline-ripple-effect-color +ion-button,css-prop,--ion-button-hue-bold-outline-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-activated-background +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-activated-color +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-default-background +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-default-border-color +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-default-color +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-disabled-background +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-focus-background +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-focus-color +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-hover-background +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-hover-color +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-ripple-effect-color +ion-button,css-prop,--ion-button-hue-bold-outline-semantic-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-bold-outline-strong-font-weight +ion-button,css-prop,--ion-button-hue-bold-solid-activated-background +ion-button,css-prop,--ion-button-hue-bold-solid-activated-box-shadow +ion-button,css-prop,--ion-button-hue-bold-solid-activated-color +ion-button,css-prop,--ion-button-hue-bold-solid-activated-opacity +ion-button,css-prop,--ion-button-hue-bold-solid-default-background +ion-button,css-prop,--ion-button-hue-bold-solid-default-border-color +ion-button,css-prop,--ion-button-hue-bold-solid-default-border-style +ion-button,css-prop,--ion-button-hue-bold-solid-default-border-width +ion-button,css-prop,--ion-button-hue-bold-solid-default-box-shadow +ion-button,css-prop,--ion-button-hue-bold-solid-default-color +ion-button,css-prop,--ion-button-hue-bold-solid-default-font-weight +ion-button,css-prop,--ion-button-hue-bold-solid-disabled-background +ion-button,css-prop,--ion-button-hue-bold-solid-disabled-opacity +ion-button,css-prop,--ion-button-hue-bold-solid-focus-background +ion-button,css-prop,--ion-button-hue-bold-solid-focus-color +ion-button,css-prop,--ion-button-hue-bold-solid-hover-background +ion-button,css-prop,--ion-button-hue-bold-solid-hover-color +ion-button,css-prop,--ion-button-hue-bold-solid-hover-opacity +ion-button,css-prop,--ion-button-hue-bold-solid-ripple-effect-color +ion-button,css-prop,--ion-button-hue-bold-solid-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-activated-background +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-activated-color +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-default-background +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-default-border-color +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-default-color +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-disabled-background +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-focus-background +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-focus-color +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-hover-background +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-hover-color +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-ripple-effect-color +ion-button,css-prop,--ion-button-hue-bold-solid-semantic-ripple-effect-opacity +ion-button,css-prop,--ion-button-hue-bold-solid-strong-font-weight +ion-button,css-prop,--ion-button-letter-spacing +ion-button,css-prop,--ion-button-margin-bottom +ion-button,css-prop,--ion-button-margin-end +ion-button,css-prop,--ion-button-margin-start +ion-button,css-prop,--ion-button-margin-top +ion-button,css-prop,--ion-button-shape-crisp-border-radius +ion-button,css-prop,--ion-button-shape-rectangular-border-radius +ion-button,css-prop,--ion-button-shape-round-border-radius +ion-button,css-prop,--ion-button-shape-soft-border-radius +ion-button,css-prop,--ion-button-size-large-font-size +ion-button,css-prop,--ion-button-size-large-icon-font-size +ion-button,css-prop,--ion-button-size-large-icon-leading-margin-bottom +ion-button,css-prop,--ion-button-size-large-icon-leading-margin-end +ion-button,css-prop,--ion-button-size-large-icon-leading-margin-start +ion-button,css-prop,--ion-button-size-large-icon-leading-margin-top +ion-button,css-prop,--ion-button-size-large-icon-only-icon-font-size +ion-button,css-prop,--ion-button-size-large-icon-only-padding-bottom +ion-button,css-prop,--ion-button-size-large-icon-only-padding-end +ion-button,css-prop,--ion-button-size-large-icon-only-padding-start +ion-button,css-prop,--ion-button-size-large-icon-only-padding-top +ion-button,css-prop,--ion-button-size-large-icon-trailing-margin-bottom +ion-button,css-prop,--ion-button-size-large-icon-trailing-margin-end +ion-button,css-prop,--ion-button-size-large-icon-trailing-margin-start +ion-button,css-prop,--ion-button-size-large-icon-trailing-margin-top +ion-button,css-prop,--ion-button-size-large-line-height +ion-button,css-prop,--ion-button-size-large-min-height +ion-button,css-prop,--ion-button-size-large-min-width +ion-button,css-prop,--ion-button-size-large-padding-bottom +ion-button,css-prop,--ion-button-size-large-padding-end +ion-button,css-prop,--ion-button-size-large-padding-start +ion-button,css-prop,--ion-button-size-large-padding-top +ion-button,css-prop,--ion-button-size-large-spinner-leading-margin-bottom +ion-button,css-prop,--ion-button-size-large-spinner-leading-margin-end +ion-button,css-prop,--ion-button-size-large-spinner-leading-margin-start +ion-button,css-prop,--ion-button-size-large-spinner-leading-margin-top +ion-button,css-prop,--ion-button-size-large-spinner-size-large-height +ion-button,css-prop,--ion-button-size-large-spinner-size-large-width +ion-button,css-prop,--ion-button-size-large-spinner-size-medium-height +ion-button,css-prop,--ion-button-size-large-spinner-size-medium-width +ion-button,css-prop,--ion-button-size-large-spinner-size-small-height +ion-button,css-prop,--ion-button-size-large-spinner-size-small-width +ion-button,css-prop,--ion-button-size-large-spinner-size-xlarge-height +ion-button,css-prop,--ion-button-size-large-spinner-size-xlarge-width +ion-button,css-prop,--ion-button-size-large-spinner-size-xsmall-height +ion-button,css-prop,--ion-button-size-large-spinner-size-xsmall-width +ion-button,css-prop,--ion-button-size-large-spinner-trailing-margin-bottom +ion-button,css-prop,--ion-button-size-large-spinner-trailing-margin-end +ion-button,css-prop,--ion-button-size-large-spinner-trailing-margin-start +ion-button,css-prop,--ion-button-size-large-spinner-trailing-margin-top +ion-button,css-prop,--ion-button-size-medium-font-size +ion-button,css-prop,--ion-button-size-medium-icon-font-size +ion-button,css-prop,--ion-button-size-medium-icon-leading-margin-bottom +ion-button,css-prop,--ion-button-size-medium-icon-leading-margin-end +ion-button,css-prop,--ion-button-size-medium-icon-leading-margin-start +ion-button,css-prop,--ion-button-size-medium-icon-leading-margin-top +ion-button,css-prop,--ion-button-size-medium-icon-only-icon-font-size +ion-button,css-prop,--ion-button-size-medium-icon-only-padding-bottom +ion-button,css-prop,--ion-button-size-medium-icon-only-padding-end +ion-button,css-prop,--ion-button-size-medium-icon-only-padding-start +ion-button,css-prop,--ion-button-size-medium-icon-only-padding-top +ion-button,css-prop,--ion-button-size-medium-icon-trailing-margin-bottom +ion-button,css-prop,--ion-button-size-medium-icon-trailing-margin-end +ion-button,css-prop,--ion-button-size-medium-icon-trailing-margin-start +ion-button,css-prop,--ion-button-size-medium-icon-trailing-margin-top +ion-button,css-prop,--ion-button-size-medium-line-height +ion-button,css-prop,--ion-button-size-medium-min-height +ion-button,css-prop,--ion-button-size-medium-min-width +ion-button,css-prop,--ion-button-size-medium-padding-bottom +ion-button,css-prop,--ion-button-size-medium-padding-end +ion-button,css-prop,--ion-button-size-medium-padding-start +ion-button,css-prop,--ion-button-size-medium-padding-top +ion-button,css-prop,--ion-button-size-medium-spinner-leading-margin-bottom +ion-button,css-prop,--ion-button-size-medium-spinner-leading-margin-end +ion-button,css-prop,--ion-button-size-medium-spinner-leading-margin-start +ion-button,css-prop,--ion-button-size-medium-spinner-leading-margin-top +ion-button,css-prop,--ion-button-size-medium-spinner-size-large-height +ion-button,css-prop,--ion-button-size-medium-spinner-size-large-width +ion-button,css-prop,--ion-button-size-medium-spinner-size-medium-height +ion-button,css-prop,--ion-button-size-medium-spinner-size-medium-width +ion-button,css-prop,--ion-button-size-medium-spinner-size-small-height +ion-button,css-prop,--ion-button-size-medium-spinner-size-small-width +ion-button,css-prop,--ion-button-size-medium-spinner-size-xlarge-height +ion-button,css-prop,--ion-button-size-medium-spinner-size-xlarge-width +ion-button,css-prop,--ion-button-size-medium-spinner-size-xsmall-height +ion-button,css-prop,--ion-button-size-medium-spinner-size-xsmall-width +ion-button,css-prop,--ion-button-size-medium-spinner-trailing-margin-bottom +ion-button,css-prop,--ion-button-size-medium-spinner-trailing-margin-end +ion-button,css-prop,--ion-button-size-medium-spinner-trailing-margin-start +ion-button,css-prop,--ion-button-size-medium-spinner-trailing-margin-top +ion-button,css-prop,--ion-button-size-small-font-size +ion-button,css-prop,--ion-button-size-small-icon-font-size +ion-button,css-prop,--ion-button-size-small-icon-leading-margin-bottom +ion-button,css-prop,--ion-button-size-small-icon-leading-margin-end +ion-button,css-prop,--ion-button-size-small-icon-leading-margin-start +ion-button,css-prop,--ion-button-size-small-icon-leading-margin-top +ion-button,css-prop,--ion-button-size-small-icon-only-icon-font-size +ion-button,css-prop,--ion-button-size-small-icon-only-padding-bottom +ion-button,css-prop,--ion-button-size-small-icon-only-padding-end +ion-button,css-prop,--ion-button-size-small-icon-only-padding-start +ion-button,css-prop,--ion-button-size-small-icon-only-padding-top +ion-button,css-prop,--ion-button-size-small-icon-trailing-margin-bottom +ion-button,css-prop,--ion-button-size-small-icon-trailing-margin-end +ion-button,css-prop,--ion-button-size-small-icon-trailing-margin-start +ion-button,css-prop,--ion-button-size-small-icon-trailing-margin-top +ion-button,css-prop,--ion-button-size-small-line-height +ion-button,css-prop,--ion-button-size-small-min-height +ion-button,css-prop,--ion-button-size-small-min-width +ion-button,css-prop,--ion-button-size-small-padding-bottom +ion-button,css-prop,--ion-button-size-small-padding-end +ion-button,css-prop,--ion-button-size-small-padding-start +ion-button,css-prop,--ion-button-size-small-padding-top +ion-button,css-prop,--ion-button-size-small-spinner-leading-margin-bottom +ion-button,css-prop,--ion-button-size-small-spinner-leading-margin-end +ion-button,css-prop,--ion-button-size-small-spinner-leading-margin-start +ion-button,css-prop,--ion-button-size-small-spinner-leading-margin-top +ion-button,css-prop,--ion-button-size-small-spinner-size-large-height +ion-button,css-prop,--ion-button-size-small-spinner-size-large-width +ion-button,css-prop,--ion-button-size-small-spinner-size-medium-height +ion-button,css-prop,--ion-button-size-small-spinner-size-medium-width +ion-button,css-prop,--ion-button-size-small-spinner-size-small-height +ion-button,css-prop,--ion-button-size-small-spinner-size-small-width +ion-button,css-prop,--ion-button-size-small-spinner-size-xlarge-height +ion-button,css-prop,--ion-button-size-small-spinner-size-xlarge-width +ion-button,css-prop,--ion-button-size-small-spinner-size-xsmall-height +ion-button,css-prop,--ion-button-size-small-spinner-size-xsmall-width +ion-button,css-prop,--ion-button-size-small-spinner-trailing-margin-bottom +ion-button,css-prop,--ion-button-size-small-spinner-trailing-margin-end +ion-button,css-prop,--ion-button-size-small-spinner-trailing-margin-start +ion-button,css-prop,--ion-button-size-small-spinner-trailing-margin-top +ion-button,css-prop,--ion-button-state-focus-ring-color +ion-button,css-prop,--ion-button-state-focus-ring-offset +ion-button,css-prop,--ion-button-state-focus-ring-style +ion-button,css-prop,--ion-button-state-focus-ring-width +ion-button,css-prop,--ion-button-text-transform +ion-button,css-prop,--ion-button-transition ion-button,part,native ion-buttons,scoped diff --git a/core/src/components.d.ts b/core/src/components.d.ts index a25784da787..77dc1472557 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -15,6 +15,7 @@ import { Hue } from "./themes/themes.interfaces"; import { IonBadgeShape, IonBadgeSize, IonBadgeVerticalPosition } from "./components/badge/badge.interfaces"; import { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./components/router/utils/interface"; import { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface"; +import { IonButtonExpand, IonButtonFill, IonButtonShape, IonButtonSize } from "./components/button/button.interface"; import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface"; import { IonChipFill, IonChipShape, IonChipSize } from "./components/chip/chip.interfaces"; import { ScrollBaseDetail, ScrollDetail } from "./components/content/content.interfaces"; @@ -61,6 +62,7 @@ export { Hue } from "./themes/themes.interfaces"; export { IonBadgeShape, IonBadgeSize, IonBadgeVerticalPosition } from "./components/badge/badge.interfaces"; export { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./components/router/utils/interface"; export { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface"; +export { IonButtonExpand, IonButtonFill, IonButtonShape, IonButtonSize } from "./components/button/button.interface"; export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface"; export { IonChipFill, IonChipShape, IonChipSize } from "./components/chip/chip.interfaces"; export { ScrollBaseDetail, ScrollDetail } from "./components/content/content.interfaces"; @@ -610,11 +612,11 @@ export namespace Components { /** * Set to `"block"` for a full-width button or to `"full"` for a full-width button with square corners and no left or right borders. */ - "expand"?: 'full' | 'block'; + "expand"?: IonButtonExpand; /** - * Set to `"clear"` for a transparent button that resembles a flat button, to `"outline"` for a transparent button with a border, or to `"solid"` for a button with a filled background. The default fill is `"solid"` except when inside of a buttons or datetime component, where the default fill is `"clear"`. + * Set to `"clear"` for a transparent button that resembles a flat button, `"outline"` for a transparent button with a border, or `"solid"` for a button with a filled background. Defaults to `"solid"` if both the fill property and theme config are unset, or `"clear"` inside of buttons, datetime, or list header. */ - "fill"?: 'clear' | 'outline' | 'solid' | 'default'; + "fill"?: IonButtonFill | 'default'; /** * The HTML form element or form element id. Used to submit a form when the button is not a child of the form. */ @@ -641,13 +643,13 @@ export namespace Components { */ "routerDirection": RouterDirection; /** - * Set to `"soft"` for a button with slightly rounded corners, `"round"` for a button with fully rounded corners, or `"rectangular"` for a button without rounded corners. Defaults to `"soft"` for the `"ios"` theme and `"round"` for all other themes. + * Set to `"crisp"` for a button with even slightly rounded corners, `"soft"` for a button with slightly rounded corners, `"round"` for a button with fully rounded corners, or `"rectangular"` for a button without rounded corners. Defaults to `"round"` if both the shape property and theme config are unset. */ - "shape"?: 'soft' | 'round' | 'rectangular'; + "shape"?: IonButtonShape; /** - * Set to `"small"` for a button with less height and padding, to `"medium"` for a button with the medium height and padding, or to `"large"` for a button with more height and padding. By default the size is unset, unless the button is inside of an item, where the size is `"small"` by default. Set the size to `"default"` inside of an item to make it a standard size button. + * Set to `"small"` for a button with less height and padding, `"medium"` for a button with the standard height and padding, or `"large"` for a button with more height and padding. Defaults to `"medium"` if both the size property and theme config are unset, or `"small"` inside of an item. `"default"` is deprecated and renders the same as `"medium"`. */ - "size"?: 'small' | 'default' | 'medium' | 'large'; + "size"?: IonButtonSize | 'default'; /** * If `true`, activates a button with a heavier font weight. * @default false @@ -657,10 +659,6 @@ export namespace Components { * Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. */ "target": string | undefined; - /** - * The theme determines the visual appearance of the component. - */ - "theme"?: "ios" | "md" | "ionic"; /** * The type of the button. * @default 'button' @@ -6467,11 +6465,11 @@ declare namespace LocalJSX { /** * Set to `"block"` for a full-width button or to `"full"` for a full-width button with square corners and no left or right borders. */ - "expand"?: 'full' | 'block'; + "expand"?: IonButtonExpand; /** - * Set to `"clear"` for a transparent button that resembles a flat button, to `"outline"` for a transparent button with a border, or to `"solid"` for a button with a filled background. The default fill is `"solid"` except when inside of a buttons or datetime component, where the default fill is `"clear"`. + * Set to `"clear"` for a transparent button that resembles a flat button, `"outline"` for a transparent button with a border, or `"solid"` for a button with a filled background. Defaults to `"solid"` if both the fill property and theme config are unset, or `"clear"` inside of buttons, datetime, or list header. */ - "fill"?: 'clear' | 'outline' | 'solid' | 'default'; + "fill"?: IonButtonFill | 'default'; /** * The HTML form element or form element id. Used to submit a form when the button is not a child of the form. */ @@ -6506,13 +6504,13 @@ declare namespace LocalJSX { */ "routerDirection"?: RouterDirection; /** - * Set to `"soft"` for a button with slightly rounded corners, `"round"` for a button with fully rounded corners, or `"rectangular"` for a button without rounded corners. Defaults to `"soft"` for the `"ios"` theme and `"round"` for all other themes. + * Set to `"crisp"` for a button with even slightly rounded corners, `"soft"` for a button with slightly rounded corners, `"round"` for a button with fully rounded corners, or `"rectangular"` for a button without rounded corners. Defaults to `"round"` if both the shape property and theme config are unset. */ - "shape"?: 'soft' | 'round' | 'rectangular'; + "shape"?: IonButtonShape; /** - * Set to `"small"` for a button with less height and padding, to `"medium"` for a button with the medium height and padding, or to `"large"` for a button with more height and padding. By default the size is unset, unless the button is inside of an item, where the size is `"small"` by default. Set the size to `"default"` inside of an item to make it a standard size button. + * Set to `"small"` for a button with less height and padding, `"medium"` for a button with the standard height and padding, or `"large"` for a button with more height and padding. Defaults to `"medium"` if both the size property and theme config are unset, or `"small"` inside of an item. `"default"` is deprecated and renders the same as `"medium"`. */ - "size"?: 'small' | 'default' | 'medium' | 'large'; + "size"?: IonButtonSize | 'default'; /** * If `true`, activates a button with a heavier font weight. * @default false @@ -6522,10 +6520,6 @@ declare namespace LocalJSX { * Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. */ "target"?: string | undefined; - /** - * The theme determines the visual appearance of the component. - */ - "theme"?: "ios" | "md" | "ionic"; /** * The type of the button. * @default 'button' @@ -10481,14 +10475,14 @@ declare namespace LocalJSX { "color": Color; "buttonType": string; "disabled": boolean; - "expand": 'full' | 'block'; - "fill": 'clear' | 'outline' | 'solid' | 'default'; + "expand": IonButtonExpand; + "fill": IonButtonFill | 'default'; "routerDirection": RouterDirection; "download": string | undefined; "href": string | undefined; "rel": string | undefined; - "shape": 'soft' | 'round' | 'rectangular'; - "size": 'small' | 'default' | 'medium' | 'large'; + "shape": IonButtonShape; + "size": IonButtonSize | 'default'; "strong": boolean; "target": string | undefined; "type": 'submit' | 'reset' | 'button'; diff --git a/core/src/components/button/_button.docs.scss b/core/src/components/button/_button.docs.scss new file mode 100644 index 00000000000..58d9d03d87b --- /dev/null +++ b/core/src/components/button/_button.docs.scss @@ -0,0 +1,237 @@ +/** + * @prop --ion-button-margin-top: Top margin of the button + * @prop --ion-button-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of the button + * @prop --ion-button-margin-bottom: Bottom margin of the button + * @prop --ion-button-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of the button + * @prop --ion-button-font-family: Font family of the button + * @prop --ion-button-letter-spacing: Letter spacing of the button + * @prop --ion-button-text-transform: Text transform of the button + * @prop --ion-button-transition: Transition of the button + * @prop --ion-button-hue-bold-solid-default-font-weight: Font weight of the `bold` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-bold-solid-strong-font-weight: Font weight of the `bold` hue and `solid` fill combination on a `strong` button + * @prop --ion-button-hue-bold-outline-default-font-weight: Font weight of the `bold` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-bold-outline-strong-font-weight: Font weight of the `bold` hue and `outline` fill combination on a `strong` button + * @prop --ion-button-hue-bold-clear-default-font-weight: Font weight of the `bold` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-bold-clear-strong-font-weight: Font weight of the `bold` hue and `clear` fill combination on a `strong` button + * @prop --ion-button-size-small-min-width: Minimum width of the `small` button size + * @prop --ion-button-size-small-min-height: Minimum height of the `small` button size, and the width and height of an icon-only button of that size + * @prop --ion-button-size-small-font-size: Font size of the `small` button size + * @prop --ion-button-size-small-line-height: Line height of the `small` button size + * @prop --ion-button-size-small-padding-top: Top padding of the `small` button size + * @prop --ion-button-size-small-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the `small` button size + * @prop --ion-button-size-small-padding-bottom: Bottom padding of the `small` button size + * @prop --ion-button-size-small-padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the `small` button size + * @prop --ion-button-size-small-icon-font-size: Font size of the icons of the `small` button size + * @prop --ion-button-size-small-icon-leading-margin-top: Top margin of an icon in the `start` slot of the `small` button size + * @prop --ion-button-size-small-icon-leading-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of an icon in the `start` slot of the `small` button size + * @prop --ion-button-size-small-icon-leading-margin-bottom: Bottom margin of an icon in the `start` slot of the `small` button size + * @prop --ion-button-size-small-icon-leading-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of an icon in the `start` slot of the `small` button size + * @prop --ion-button-size-small-icon-trailing-margin-top: Top margin of an icon in the `end` slot of the `small` button size + * @prop --ion-button-size-small-icon-trailing-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of an icon in the `end` slot of the `small` button size + * @prop --ion-button-size-small-icon-trailing-margin-bottom: Bottom margin of an icon in the `end` slot of the `small` button size + * @prop --ion-button-size-small-icon-trailing-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of an icon in the `end` slot of the `small` button size + * @prop --ion-button-size-small-icon-only-padding-top: Top padding of an icon-only button of the `small` size + * @prop --ion-button-size-small-icon-only-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of an icon-only button of the `small` size + * @prop --ion-button-size-small-icon-only-padding-bottom: Bottom padding of an icon-only button of the `small` size + * @prop --ion-button-size-small-icon-only-padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of an icon-only button of the `small` size + * @prop --ion-button-size-small-icon-only-icon-font-size: Font size of the icon in an icon-only button of the `small` size + * @prop --ion-button-size-small-spinner-size-xsmall-width: Width of an `xsmall` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-size-xsmall-height: Height of an `xsmall` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-size-small-width: Width of a `small` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-size-small-height: Height of a `small` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-size-medium-width: Width of a `medium` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-size-medium-height: Height of a `medium` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-size-large-width: Width of a `large` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-size-large-height: Height of a `large` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-size-xlarge-width: Width of an `xlarge` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-size-xlarge-height: Height of an `xlarge` spinner in the `start`, `end` or `icon-only` slot of the `small` button size + * @prop --ion-button-size-small-spinner-leading-margin-top: Top margin of a spinner in the `start` slot of the `small` button size + * @prop --ion-button-size-small-spinner-leading-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a spinner in the `start` slot of the `small` button size + * @prop --ion-button-size-small-spinner-leading-margin-bottom: Bottom margin of a spinner in the `start` slot of the `small` button size + * @prop --ion-button-size-small-spinner-leading-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a spinner in the `start` slot of the `small` button size + * @prop --ion-button-size-small-spinner-trailing-margin-top: Top margin of a spinner in the `end` slot of the `small` button size + * @prop --ion-button-size-small-spinner-trailing-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a spinner in the `end` slot of the `small` button size + * @prop --ion-button-size-small-spinner-trailing-margin-bottom: Bottom margin of a spinner in the `end` slot of the `small` button size + * @prop --ion-button-size-small-spinner-trailing-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a spinner in the `end` slot of the `small` button size + * @prop --ion-button-size-medium-min-width: Minimum width of the `medium` button size + * @prop --ion-button-size-medium-min-height: Minimum height of the `medium` button size, and the width and height of an icon-only button of that size + * @prop --ion-button-size-medium-font-size: Font size of the `medium` button size + * @prop --ion-button-size-medium-line-height: Line height of the `medium` button size + * @prop --ion-button-size-medium-padding-top: Top padding of the `medium` button size + * @prop --ion-button-size-medium-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the `medium` button size + * @prop --ion-button-size-medium-padding-bottom: Bottom padding of the `medium` button size + * @prop --ion-button-size-medium-padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the `medium` button size + * @prop --ion-button-size-medium-icon-font-size: Font size of the icons of the `medium` button size + * @prop --ion-button-size-medium-icon-leading-margin-top: Top margin of an icon in the `start` slot of the `medium` button size + * @prop --ion-button-size-medium-icon-leading-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of an icon in the `start` slot of the `medium` button size + * @prop --ion-button-size-medium-icon-leading-margin-bottom: Bottom margin of an icon in the `start` slot of the `medium` button size + * @prop --ion-button-size-medium-icon-leading-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of an icon in the `start` slot of the `medium` button size + * @prop --ion-button-size-medium-icon-trailing-margin-top: Top margin of an icon in the `end` slot of the `medium` button size + * @prop --ion-button-size-medium-icon-trailing-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of an icon in the `end` slot of the `medium` button size + * @prop --ion-button-size-medium-icon-trailing-margin-bottom: Bottom margin of an icon in the `end` slot of the `medium` button size + * @prop --ion-button-size-medium-icon-trailing-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of an icon in the `end` slot of the `medium` button size + * @prop --ion-button-size-medium-icon-only-padding-top: Top padding of an icon-only button of the `medium` size + * @prop --ion-button-size-medium-icon-only-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of an icon-only button of the `medium` size + * @prop --ion-button-size-medium-icon-only-padding-bottom: Bottom padding of an icon-only button of the `medium` size + * @prop --ion-button-size-medium-icon-only-padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of an icon-only button of the `medium` size + * @prop --ion-button-size-medium-icon-only-icon-font-size: Font size of the icon in an icon-only button of the `medium` size + * @prop --ion-button-size-medium-spinner-size-xsmall-width: Width of an `xsmall` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-size-xsmall-height: Height of an `xsmall` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-size-small-width: Width of a `small` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-size-small-height: Height of a `small` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-size-medium-width: Width of a `medium` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-size-medium-height: Height of a `medium` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-size-large-width: Width of a `large` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-size-large-height: Height of a `large` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-size-xlarge-width: Width of an `xlarge` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-size-xlarge-height: Height of an `xlarge` spinner in the `start`, `end` or `icon-only` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-leading-margin-top: Top margin of a spinner in the `start` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-leading-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a spinner in the `start` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-leading-margin-bottom: Bottom margin of a spinner in the `start` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-leading-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a spinner in the `start` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-trailing-margin-top: Top margin of a spinner in the `end` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-trailing-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a spinner in the `end` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-trailing-margin-bottom: Bottom margin of a spinner in the `end` slot of the `medium` button size + * @prop --ion-button-size-medium-spinner-trailing-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a spinner in the `end` slot of the `medium` button size + * @prop --ion-button-size-large-min-width: Minimum width of the `large` button size + * @prop --ion-button-size-large-min-height: Minimum height of the `large` button size, and the width and height of an icon-only button of that size + * @prop --ion-button-size-large-font-size: Font size of the `large` button size + * @prop --ion-button-size-large-line-height: Line height of the `large` button size + * @prop --ion-button-size-large-padding-top: Top padding of the `large` button size + * @prop --ion-button-size-large-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the `large` button size + * @prop --ion-button-size-large-padding-bottom: Bottom padding of the `large` button size + * @prop --ion-button-size-large-padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the `large` button size + * @prop --ion-button-size-large-icon-font-size: Font size of the icons of the `large` button size + * @prop --ion-button-size-large-icon-leading-margin-top: Top margin of an icon in the `start` slot of the `large` button size + * @prop --ion-button-size-large-icon-leading-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of an icon in the `start` slot of the `large` button size + * @prop --ion-button-size-large-icon-leading-margin-bottom: Bottom margin of an icon in the `start` slot of the `large` button size + * @prop --ion-button-size-large-icon-leading-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of an icon in the `start` slot of the `large` button size + * @prop --ion-button-size-large-icon-trailing-margin-top: Top margin of an icon in the `end` slot of the `large` button size + * @prop --ion-button-size-large-icon-trailing-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of an icon in the `end` slot of the `large` button size + * @prop --ion-button-size-large-icon-trailing-margin-bottom: Bottom margin of an icon in the `end` slot of the `large` button size + * @prop --ion-button-size-large-icon-trailing-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of an icon in the `end` slot of the `large` button size + * @prop --ion-button-size-large-icon-only-padding-top: Top padding of an icon-only button of the `large` size + * @prop --ion-button-size-large-icon-only-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of an icon-only button of the `large` size + * @prop --ion-button-size-large-icon-only-padding-bottom: Bottom padding of an icon-only button of the `large` size + * @prop --ion-button-size-large-icon-only-padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of an icon-only button of the `large` size + * @prop --ion-button-size-large-icon-only-icon-font-size: Font size of the icon in an icon-only button of the `large` size + * @prop --ion-button-size-large-spinner-size-xsmall-width: Width of an `xsmall` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-size-xsmall-height: Height of an `xsmall` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-size-small-width: Width of a `small` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-size-small-height: Height of a `small` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-size-medium-width: Width of a `medium` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-size-medium-height: Height of a `medium` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-size-large-width: Width of a `large` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-size-large-height: Height of a `large` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-size-xlarge-width: Width of an `xlarge` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-size-xlarge-height: Height of an `xlarge` spinner in the `start`, `end` or `icon-only` slot of the `large` button size + * @prop --ion-button-size-large-spinner-leading-margin-top: Top margin of a spinner in the `start` slot of the `large` button size + * @prop --ion-button-size-large-spinner-leading-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a spinner in the `start` slot of the `large` button size + * @prop --ion-button-size-large-spinner-leading-margin-bottom: Bottom margin of a spinner in the `start` slot of the `large` button size + * @prop --ion-button-size-large-spinner-leading-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a spinner in the `start` slot of the `large` button size + * @prop --ion-button-size-large-spinner-trailing-margin-top: Top margin of a spinner in the `end` slot of the `large` button size + * @prop --ion-button-size-large-spinner-trailing-margin-end: Right margin if direction is left-to-right, and left margin if direction is right-to-left of a spinner in the `end` slot of the `large` button size + * @prop --ion-button-size-large-spinner-trailing-margin-bottom: Bottom margin of a spinner in the `end` slot of the `large` button size + * @prop --ion-button-size-large-spinner-trailing-margin-start: Left margin if direction is left-to-right, and right margin if direction is right-to-left of a spinner in the `end` slot of the `large` button size + * @prop --ion-button-shape-crisp-border-radius: Border radius of the `crisp` button shape + * @prop --ion-button-shape-soft-border-radius: Border radius of the `soft` button shape + * @prop --ion-button-shape-round-border-radius: Border radius of the `round` button shape + * @prop --ion-button-shape-rectangular-border-radius: Border radius of the `rectangular` button shape + * @prop --ion-button-state-focus-ring-width: Width of the focus ring + * @prop --ion-button-state-focus-ring-style: Style of the focus ring + * @prop --ion-button-state-focus-ring-color: Color of the focus ring + * @prop --ion-button-state-focus-ring-offset: Offset of the focus ring + * @prop --ion-button-hue-bold-outline-default-border-color: Border color of the `bold` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-bold-outline-default-color: Text color of the `bold` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-bold-clear-default-color: Text color of the `bold` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-bold-solid-default-background: Background of the `bold` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-bold-solid-default-color: Text color of the `bold` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-bold-solid-default-border-width: Border width of the `bold` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-bold-solid-default-border-style: Border style of the `bold` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-bold-solid-default-border-color: Border color of the `bold` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-bold-solid-default-box-shadow: Box shadow of the `bold` hue and `solid` fill combination in the default state + * @prop --ion-button-hue-bold-solid-ripple-effect-color: Ripple color of the `bold` hue and `solid` fill combination + * @prop --ion-button-hue-bold-solid-ripple-effect-opacity: Ripple opacity of the `bold` hue and `solid` fill combination + * @prop --ion-button-hue-bold-solid-semantic-default-border-color: Border color of the `bold` hue and `solid` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-bold-solid-semantic-default-background: Background of the `bold` hue and `solid` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-bold-solid-semantic-default-color: Text color of the `bold` hue and `solid` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-bold-solid-semantic-ripple-effect-color: Ripple color of the `bold` hue and `solid` fill combination when a semantic color is applied + * @prop --ion-button-hue-bold-solid-semantic-ripple-effect-opacity: Ripple opacity of the `bold` hue and `solid` fill combination when a semantic color is applied + * @prop --ion-button-hue-bold-solid-focus-color: Text color of the `bold` hue and `solid` fill combination in the focus state + * @prop --ion-button-hue-bold-solid-focus-background: Background of the `bold` hue and `solid` fill combination in the focus state + * @prop --ion-button-hue-bold-solid-semantic-focus-background: Background of the `bold` hue and `solid` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-bold-solid-semantic-focus-color: Text color of the `bold` hue and `solid` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-bold-solid-hover-color: Text color of the `bold` hue and `solid` fill combination in the hover state + * @prop --ion-button-hue-bold-solid-hover-opacity: Opacity of the whole button of the `bold` hue and `solid` fill combination in the hover state + * @prop --ion-button-hue-bold-solid-hover-background: Background of the `bold` hue and `solid` fill combination in the hover state + * @prop --ion-button-hue-bold-solid-semantic-hover-background: Background of the `bold` hue and `solid` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-bold-solid-semantic-hover-color: Text color of the `bold` hue and `solid` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-bold-solid-activated-color: Text color of the `bold` hue and `solid` fill combination in the activated state + * @prop --ion-button-hue-bold-solid-activated-opacity: Opacity of the whole button of the `bold` hue and `solid` fill combination in the activated state + * @prop --ion-button-hue-bold-solid-activated-background: Background of the `bold` hue and `solid` fill combination in the activated state + * @prop --ion-button-hue-bold-solid-activated-box-shadow: Box shadow of the `bold` hue and `solid` fill combination in the activated state + * @prop --ion-button-hue-bold-solid-semantic-activated-background: Background of the `bold` hue and `solid` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-bold-solid-semantic-activated-color: Text color of the `bold` hue and `solid` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-bold-solid-disabled-background: Background of the `bold` hue and `solid` fill combination in the disabled state + * @prop --ion-button-hue-bold-solid-disabled-opacity: Opacity of the whole button of the `bold` hue and `solid` fill combination in the disabled state + * @prop --ion-button-hue-bold-solid-semantic-disabled-background: Background of the `bold` hue and `solid` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-default-border-width: Border width of the `bold` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-bold-outline-default-border-style: Border style of the `bold` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-bold-outline-default-background: Background of the `bold` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-bold-outline-default-box-shadow: Box shadow of the `bold` hue and `outline` fill combination in the default state + * @prop --ion-button-hue-bold-outline-ripple-effect-color: Ripple color of the `bold` hue and `outline` fill combination + * @prop --ion-button-hue-bold-outline-ripple-effect-opacity: Ripple opacity of the `bold` hue and `outline` fill combination + * @prop --ion-button-hue-bold-outline-semantic-default-border-color: Border color of the `bold` hue and `outline` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-semantic-default-background: Background of the `bold` hue and `outline` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-semantic-default-color: Text color of the `bold` hue and `outline` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-semantic-ripple-effect-color: Ripple color of the `bold` hue and `outline` fill combination when a semantic color is applied + * @prop --ion-button-hue-bold-outline-semantic-ripple-effect-opacity: Ripple opacity of the `bold` hue and `outline` fill combination when a semantic color is applied + * @prop --ion-button-hue-bold-outline-focus-color: Text color of the `bold` hue and `outline` fill combination in the focus state + * @prop --ion-button-hue-bold-outline-focus-background: Background of the `bold` hue and `outline` fill combination in the focus state + * @prop --ion-button-hue-bold-outline-semantic-focus-background: Background of the `bold` hue and `outline` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-semantic-focus-color: Text color of the `bold` hue and `outline` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-hover-color: Text color of the `bold` hue and `outline` fill combination in the hover state + * @prop --ion-button-hue-bold-outline-hover-opacity: Opacity of the whole button of the `bold` hue and `outline` fill combination in the hover state + * @prop --ion-button-hue-bold-outline-hover-background: Background of the `bold` hue and `outline` fill combination in the hover state + * @prop --ion-button-hue-bold-outline-semantic-hover-background: Background of the `bold` hue and `outline` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-semantic-hover-color: Text color of the `bold` hue and `outline` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-activated-color: Text color of the `bold` hue and `outline` fill combination in the activated state + * @prop --ion-button-hue-bold-outline-activated-opacity: Opacity of the whole button of the `bold` hue and `outline` fill combination in the activated state + * @prop --ion-button-hue-bold-outline-activated-background: Background of the `bold` hue and `outline` fill combination in the activated state + * @prop --ion-button-hue-bold-outline-activated-box-shadow: Box shadow of the `bold` hue and `outline` fill combination in the activated state + * @prop --ion-button-hue-bold-outline-semantic-activated-background: Background of the `bold` hue and `outline` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-semantic-activated-color: Text color of the `bold` hue and `outline` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-bold-outline-disabled-background: Background of the `bold` hue and `outline` fill combination in the disabled state + * @prop --ion-button-hue-bold-outline-disabled-opacity: Opacity of the whole button of the `bold` hue and `outline` fill combination in the disabled state + * @prop --ion-button-hue-bold-outline-semantic-disabled-background: Background of the `bold` hue and `outline` fill combination in the disabled state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-default-border-width: Border width of the `bold` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-bold-clear-default-border-style: Border style of the `bold` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-bold-clear-default-border-color: Border color of the `bold` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-bold-clear-default-background: Background of the `bold` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-bold-clear-default-box-shadow: Box shadow of the `bold` hue and `clear` fill combination in the default state + * @prop --ion-button-hue-bold-clear-ripple-effect-color: Ripple color of the `bold` hue and `clear` fill combination + * @prop --ion-button-hue-bold-clear-ripple-effect-opacity: Ripple opacity of the `bold` hue and `clear` fill combination + * @prop --ion-button-hue-bold-clear-semantic-default-border-color: Border color of the `bold` hue and `clear` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-semantic-default-background: Background of the `bold` hue and `clear` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-semantic-default-color: Text color of the `bold` hue and `clear` fill combination in the default state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-semantic-ripple-effect-color: Ripple color of the `bold` hue and `clear` fill combination when a semantic color is applied + * @prop --ion-button-hue-bold-clear-semantic-ripple-effect-opacity: Ripple opacity of the `bold` hue and `clear` fill combination when a semantic color is applied + * @prop --ion-button-hue-bold-clear-focus-color: Text color of the `bold` hue and `clear` fill combination in the focus state + * @prop --ion-button-hue-bold-clear-focus-background: Background of the `bold` hue and `clear` fill combination in the focus state + * @prop --ion-button-hue-bold-clear-semantic-focus-background: Background of the `bold` hue and `clear` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-semantic-focus-color: Text color of the `bold` hue and `clear` fill combination in the focus state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-hover-color: Text color of the `bold` hue and `clear` fill combination in the hover state + * @prop --ion-button-hue-bold-clear-hover-opacity: Opacity of the whole button of the `bold` hue and `clear` fill combination in the hover state + * @prop --ion-button-hue-bold-clear-hover-background: Background of the `bold` hue and `clear` fill combination in the hover state + * @prop --ion-button-hue-bold-clear-semantic-hover-background: Background of the `bold` hue and `clear` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-semantic-hover-color: Text color of the `bold` hue and `clear` fill combination in the hover state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-activated-color: Text color of the `bold` hue and `clear` fill combination in the activated state + * @prop --ion-button-hue-bold-clear-activated-opacity: Opacity of the whole button of the `bold` hue and `clear` fill combination in the activated state + * @prop --ion-button-hue-bold-clear-activated-background: Background of the `bold` hue and `clear` fill combination in the activated state + * @prop --ion-button-hue-bold-clear-activated-box-shadow: Box shadow of the `bold` hue and `clear` fill combination in the activated state + * @prop --ion-button-hue-bold-clear-semantic-activated-background: Background of the `bold` hue and `clear` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-semantic-activated-color: Text color of the `bold` hue and `clear` fill combination in the activated state when a semantic color is applied + * @prop --ion-button-hue-bold-clear-disabled-background: Background of the `bold` hue and `clear` fill combination in the disabled state + * @prop --ion-button-hue-bold-clear-disabled-opacity: Opacity of the whole button of the `bold` hue and `clear` fill combination in the disabled state + * @prop --ion-button-hue-bold-clear-semantic-disabled-background: Background of the `bold` hue and `clear` fill combination in the disabled state when a semantic color is applied + */ diff --git a/core/src/components/button/button.common.scss b/core/src/components/button/button.common.scss deleted file mode 100644 index cfbb699eb0e..00000000000 --- a/core/src/components/button/button.common.scss +++ /dev/null @@ -1,314 +0,0 @@ -@import "../../themes/mixins"; -@import "../../themes/functions.color"; - -// Button -// -------------------------------------------------- - -:host { - /** - * @prop --background: Background of the button - * @prop --background-activated: Background of the button when pressed. Note: setting this will interfere with the Material Design ripple. - * @prop --background-activated-opacity: Opacity of the button when pressed - * @prop --background-focused: Background of the button when focused with the tab key - * @prop --background-focused-opacity: Opacity of the button when focused with the tab key - * @prop --background-hover: Background of the button on hover - * @prop --background-hover-opacity: Opacity of the background on hover - * - * @prop --color: Text color of the button - * @prop --color-activated: Text color of the button when pressed - * @prop --color-focused: Text color of the button when focused with the tab key - * @prop --color-hover: Text color of the button when hover - * - * @prop --transition: Transition of the button - * - * @prop --border-radius: Border radius of the button - * @prop --border-width: Border width of the button - * @prop --border-style: Border style of the button - * @prop --border-color: Border color of the button - * - * @prop --ripple-color: Color of the button ripple effect - * - * @prop --box-shadow: Box shadow of the button - * @prop --opacity: Opacity of the button - * - * @prop --padding-top: Top padding of the button - * @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the button - * @prop --padding-bottom: Bottom padding of the button - * @prop --padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the button - */ - --overflow: hidden; - --ripple-color: currentColor; - --border-width: initial; - --border-color: initial; - --border-style: initial; - --color-activated: var(--color); - --color-focused: var(--color); - --color-hover: var(--color); - --box-shadow: none; - - display: inline-block; - - width: auto; - - color: var(--color); - - text-align: center; - text-decoration: none; - - white-space: normal; - - user-select: none; - vertical-align: top; // the better option for most scenarios - vertical-align: -webkit-baseline-middle; // the best for those that support it - - font-kerning: none; -} - -:host(.button-disabled) { - cursor: default; - opacity: 0.5; - pointer-events: none; -} - -// Outline Button -// -------------------------------------------------- - -// Default Outline Color -:host(.button-outline) { - --background: transparent; -} - -// Clear Button -// -------------------------------------------------- - -// Default Clear Color -:host(.button-clear) { - --border-width: 0; - --background: transparent; -} - -// Block Button -// -------------------------------------------------- - -:host(.button-block) { - display: block; -} - -:host(.button-block) .button-native { - @include margin-horizontal(0); - - width: 100%; - - clear: both; - - contain: content; -} - -:host(.button-block) .button-native::after { - clear: both; -} - -// Full Button -// -------------------------------------------------- - -:host(.button-full) { - display: block; -} - -:host(.button-full) .button-native { - @include margin-horizontal(0); - - width: 100%; - - contain: content; -} - -:host(.button-full:not(.button-round)) .button-native { - @include border-radius(0); - - border-right-width: 0; - border-left-width: 0; -} - -.button-native { - @include border-radius(var(--border-radius)); - @include font-smoothing(); - @include margin(0); - @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); - @include text-inherit(); - - display: flex; - - position: relative; - - align-items: center; - - width: 100%; - height: 100%; - - min-height: inherit; - - transition: var(--transition); - - border-width: var(--border-width); - border-style: var(--border-style); - border-color: var(--border-color); - - outline: none; - - background: var(--background); - - line-height: 1; - - box-shadow: var(--box-shadow); - - contain: layout style; - cursor: pointer; - - opacity: var(--opacity); - overflow: var(--overflow); - - z-index: 0; - box-sizing: border-box; - appearance: none; -} - -.button-native::-moz-focus-inner { - border: 0; -} - -.button-inner { - display: flex; - position: relative; - - flex-flow: row nowrap; - flex-shrink: 0; - align-items: center; - justify-content: center; - - width: 100%; - height: 100%; - - z-index: 1; -} - -// Button Slots -// -------------------------------------------------- - -::slotted([slot="start"]), -::slotted([slot="end"]) { - flex-shrink: 0; -} - -// Button Icons & Spinners -// -------------------------------------------------- - -::slotted(ion-icon), -::slotted(ion-spinner) { - pointer-events: none; -} - -::slotted(ion-spinner) { - color: currentColor; -} - -// Button Ripple effect -// -------------------------------------------------- - -ion-ripple-effect { - color: var(--ripple-color); -} - -// Button: States -// -------------------------------------------------- - -.button-native::after { - @include button-state(); -} - -// Button Focused -:host(.ion-focused) { - color: var(--color-focused); -} - -:host(.ion-focused) .button-native::after { - background: var(--background-focused); - - opacity: var(--background-focused-opacity); -} - -// Button Hover -@media (any-hover: hover) { - :host(:hover) { - color: var(--color-hover); - } - - :host(:hover) .button-native::after { - background: var(--background-hover); - - opacity: var(--background-hover-opacity); - } -} - -// Button Activated -:host(.ion-activated) { - color: var(--color-activated); -} - -:host(.ion-activated) .button-native::after { - background: var(--background-activated); - - opacity: var(--background-activated-opacity); -} - -// Button Colors -// -------------------------------------------------- - -// Solid Button with Color -:host(.button-solid.ion-color) .button-native { - background: current-color(base); - color: current-color(contrast); -} - -// Outline Button with Color -:host(.button-outline.ion-color) .button-native { - border-color: current-color(base); - - background: transparent; - color: current-color(base); -} - -// Clear Button with Color -:host(.button-clear.ion-color) .button-native { - background: transparent; - color: current-color(foreground); -} - -// Button in Toolbar -// -------------------------------------------------- - -:host(.in-toolbar:not(.ion-color):not(.in-toolbar-color)) .button-native { - color: #{var(--ion-toolbar-color, var(--color))}; -} - -:host(.button-outline.in-toolbar:not(.ion-color):not(.in-toolbar-color)) .button-native { - border-color: #{var(--ion-toolbar-color, var(--color, var(--border-color)))}; -} - -:host(.button-solid.in-toolbar:not(.ion-color):not(.in-toolbar-color)) .button-native { - background: #{var(--ion-toolbar-color, var(--background))}; - color: #{var(--ion-toolbar-background, var(--color))}; -} - -.button-native ion-ripple-effect, -.button-native::after { - @include border-radius(inherit); -} - -// Button Slotted Elements -// --------------------------------------------- - -// Anchored Badge -:host(.button-has-badge) .button-native { - --overflow: visible; -} diff --git a/core/src/components/button/button.interface.ts b/core/src/components/button/button.interface.ts new file mode 100644 index 00000000000..0a4b642e344 --- /dev/null +++ b/core/src/components/button/button.interface.ts @@ -0,0 +1,229 @@ +import type { Hue, IonMargin, IonPadding } from '../../themes/themes.interfaces'; +import type { SpinnerSize } from '../spinner/spinner.interfaces'; + +export const ION_BUTTON_FILLS = ['clear', 'outline', 'solid'] as const; +export type IonButtonFill = (typeof ION_BUTTON_FILLS)[number]; + +export const ION_BUTTON_SHAPES = ['crisp', 'soft', 'round', 'rectangular'] as const; +export type IonButtonShape = (typeof ION_BUTTON_SHAPES)[number]; + +export const ION_BUTTON_SIZES = ['small', 'medium', 'large'] as const; +export type IonButtonSize = (typeof ION_BUTTON_SIZES)[number]; + +export const ION_BUTTON_EXPANDS = ['block', 'full'] as const; +export type IonButtonExpand = (typeof ION_BUTTON_EXPANDS)[number]; + +export type IonButtonRecipe = { + margin?: IonMargin; + transition?: string; + + font?: { + family?: string; + }; + + letter?: { + spacing?: string | number; + }; + + text?: { + transform?: string; + }; + + // Hues with fills + hue?: { + [K in Hue]?: IonButtonFillDefinition; + }; + + // Sizes + size?: { + [K in IonButtonSize]?: IonButtonSizeDefinition; + }; + + // Shapes + shape?: { + [K in IonButtonShape]?: IonButtonShapeDefinition; + }; + + // Shared states + state?: { + focus?: { + ring?: { + color?: string; + offset?: string | number; + style?: string; + width?: string | number; + }; + }; + }; +}; + +type IonButtonFillDefinition = { + [K in IonButtonFill]?: IonButtonStateDefinition & { + default?: { + font?: { + weight?: string | number; + }; + }; + + /** A `strong` button */ + strong?: { + font?: { + weight?: string | number; + }; + }; + + rippleEffect?: IonButtonRippleEffectDefinition; + + /** When the button has a `color` */ + semantic?: IonButtonSemanticDefinition & { + rippleEffect?: IonButtonRippleEffectDefinition; + }; + }; +}; + +type IonButtonStateDefinition = { + default?: IonButtonColorDefinition & { + border?: { + color?: string; + style?: string; + width?: string | number; + }; + + box?: { + shadow?: string; + }; + }; + + hover?: IonButtonColorDefinition & { + /** The whole button, for fills that fade instead of changing color */ + opacity?: string | number; + }; + + focus?: IonButtonColorDefinition; + + activated?: IonButtonColorDefinition & { + /** The whole button, for fills that fade instead of changing color */ + opacity?: string | number; + + box?: { + shadow?: string; + }; + }; + + disabled?: { + background?: string; + + /** The whole button */ + opacity?: string | number; + }; +}; + +/** The states of a button with a `color` */ +type IonButtonSemanticDefinition = { + default?: IonButtonColorDefinition & { + border?: { + color?: string; + }; + }; + + hover?: IonButtonColorDefinition; + + focus?: IonButtonColorDefinition; + + activated?: IonButtonColorDefinition; + + disabled?: { + background?: string; + }; +}; + +/** The ripple shown while the button is pressed, which only renders in `md` mode */ +type IonButtonRippleEffectDefinition = { + color?: string; + opacity?: string | number; +}; + +type IonButtonColorDefinition = { + background?: string; + color?: string; +}; + +type IonButtonIconDefinition = { + font?: { + size?: string | number; + }; + + /** Icon in the `start` slot */ + leading?: { + margin?: IonMargin; + }; + + /** Icon in the `end` slot */ + trailing?: { + margin?: IonMargin; + }; +}; + +/** A spinner in the `start`, `end` or `icon-only` slot */ +type IonButtonSpinnerDefinition = { + /** Unset sizes keep the spinner's own size */ + size?: { + [K in SpinnerSize]?: { + height?: string | number; + width?: string | number; + }; + }; + + /** Spinner in the `start` slot */ + leading?: { + margin?: IonMargin; + }; + + /** Spinner in the `end` slot */ + trailing?: { + margin?: IonMargin; + }; +}; + +/** A button whose only content is an icon in the `icon-only` slot */ +type IonButtonIconOnlyDefinition = { + padding?: IonPadding; + + icon?: { + font?: { + size?: string | number; + }; + }; +}; + +type IonButtonSizeDefinition = { + font?: { + size?: string | number; + }; + + line?: { + height?: string | number; + }; + + /** Also the width and height of an icon-only button, so it's a square as tall as a text button */ + minHeight?: string | number; + minWidth?: string | number; + padding?: IonPadding; + + icon?: IonButtonIconDefinition; + iconOnly?: IonButtonIconOnlyDefinition; + spinner?: IonButtonSpinnerDefinition; +}; + +type IonButtonShapeDefinition = { + border?: { + radius?: string | number; + }; +}; + +export type IonButtonConfig = { + fill?: IonButtonFill; + hue?: Hue; + shape?: IonButtonShape; + size?: IonButtonSize; +}; diff --git a/core/src/components/button/button.ionic.scss b/core/src/components/button/button.ionic.scss deleted file mode 100644 index c1bf4e83728..00000000000 --- a/core/src/components/button/button.ionic.scss +++ /dev/null @@ -1,309 +0,0 @@ -@use "../../themes/ionic/ionic.globals.scss" as globals; -@use "./button.common"; - -// Ionic Button -// ------------------------------------------------------------------------------- -:host { - --padding-top: #{globals.$ion-space-0}; - --padding-end: #{globals.$ion-space-0}; - --padding-bottom: var(--padding-top); - --padding-start: var(--padding-end); - --focus-ring-color: #{globals.$ion-border-focus-default}; - --focus-ring-width: #{globals.$ion-border-size-050}; - - position: relative; - - // Target area - &::after { - @include globals.position(50%, 0, null, 0); - position: absolute; - - height: 100%; - min-height: inherit; - - transform: translateY(-50%); - - content: ""; - - cursor: pointer; - - z-index: 1; - } -} - -// Button Fills -// ------------------------------------------------------------------------------- - -// Default Clear Color -:host(.button-clear) { - --color: #{globals.ion-color(primary, foreground)}; -} - -// Solid Button -// -------------------------------------------------- - -:host(.button-solid) { - --background-activated: #{globals.ion-color(primary, shade)}; - --background-hover: #{globals.ion-color(primary, shade)}; - --background-focused: transparent; - --background-hover-opacity: 1; - --background: #{globals.ion-color(primary, base)}; - --color: #{globals.ion-color(primary, contrast)}; - --ripple-color: var(--background-activated); -} - -// Outline Button -// -------------------------------------------------- - -:host(.button-outline) { - --border-width: #{globals.$ion-border-size-025}; - --border-style: solid; - --background-activated: #{globals.ion-color(primary, shade, $subtle: true)}; - --background-focused: transparent; - --background-hover: #{globals.ion-color(primary, shade, $subtle: true)}; - --background-hover-opacity: 1; - --border-color: #{globals.ion-color(primary, base)}; - --color: #{globals.ion-color(primary, base)}; - --ripple-color: var(--background-activated); -} - -// Warning Outline Button - use foreground color for text and border -:host(.button-outline.ion-color-warning) .button-native { - border-color: globals.ion-color(warning, foreground); - - color: globals.ion-color(warning, foreground); -} - -// Clear Button -// -------------------------------------------------- - -:host(.button-clear) { - --background-activated: #{globals.ion-color(primary, shade, $subtle: true)}; - --background-focused: transparent; - --background-hover: #{globals.ion-color(primary, shade, $subtle: true)}; - --background-hover-opacity: 1; - --color: #{globals.ion-color(primary, foreground)}; - --ripple-color: var(--background-activated); -} - -// Ripple Effect -// ------------------------------------------------------------------------------- - -// Set via the custom property (not `opacity` directly) because the ripple's -// opacity is animated in @keyframes, which would override a direct value. -:host(.button-solid) ion-ripple-effect, -:host(.button-outline) ion-ripple-effect, -:host(.button-clear) ion-ripple-effect { - --ion-ripple-effect-opacity: var(--background-activated-opacity, 1); -} - -:host(.button-solid.ion-color) ion-ripple-effect { - color: globals.current-color(shade); -} - -:host(.button-outline.ion-color) ion-ripple-effect, -:host(.button-clear.ion-color) ion-ripple-effect { - color: globals.current-color(shade, $subtle: true); -} - -// Button Sizes -// ------------------------------------------------------------------------------- - -/* Small Button */ -:host(.button-small) { - --padding-end: #{globals.$ion-space-400}; - - @include globals.typography(globals.$ion-body-action-sm); - min-width: globals.$ion-scale-1600; - - min-height: #{globals.$ion-scale-1000}; -} - -/* Medium Button */ -:host(.button-medium) { - --padding-end: #{globals.$ion-space-500}; - - @include globals.typography(globals.$ion-body-action-md); - min-width: globals.$ion-scale-1800; - - min-height: #{globals.$ion-scale-1200}; -} - -/* Large Button */ -:host(.button-large) { - --padding-end: #{globals.$ion-space-700}; - - @include globals.typography(globals.$ion-body-action-lg); - min-width: globals.$ion-scale-2000; - - min-height: #{globals.$ion-scale-1400}; -} - -// Button with Icons -// ------------------------------------------------------------------------------- - -/* Button containing only an icon */ -:host(.button-small), -:host(.button-medium) { - ::slotted(ion-icon[slot="start"]), - ::slotted(ion-icon[slot="end"]), - ::slotted(ion-icon[slot="icon-only"]) { - font-size: globals.$ion-font-size-500; - } -} - -:host(.button-large) { - ::slotted(ion-icon[slot="start"]), - ::slotted(ion-icon[slot="end"]), - ::slotted(ion-icon[slot="icon-only"]) { - font-size: globals.$ion-font-size-600; - } -} - -:host(.button-has-icon-only) { - --padding-end: var(--padding-top); - min-width: unset; - - aspect-ratio: 1 / 1; -} - -// Button with Spinner -// ------------------------------------------------------------------------------- - -/* Button containing only a spinner */ -::slotted(ion-spinner[slot="start"]), -::slotted(ion-spinner[slot="end"]), -::slotted(ion-spinner[slot="icon-only"]) { - width: globals.$ion-scale-500; - height: globals.$ion-scale-500; -} - -// Button Shapes -// ------------------------------------------------------------------------------- - -// Soft Button -// -------------------------------------------------- - -:host(.button-soft) { - --border-radius: #{globals.$ion-shape-soft-xs}; -} - -// Round Button -// -------------------------------------------------- - -:host(.button-round) { - --border-radius: #{globals.$ion-shape-round-xs}; -} - -// Rectangular Button -// -------------------------------------------------- - -:host(.button-rectangular) { - --border-radius: #{globals.$ion-shape-rectangular-xs}; -} - -// Button: Focus -// ------------------------------------------------------------------------------- - -// Only show the focus ring when the button is focused -:host(.ion-focused) .button-native { - outline: var(--focus-ring-width) solid var(--focus-ring-color); - outline-offset: globals.$ion-border-size-050; -} - -// Button: Activated -// -------------------------------------------------- - -:host(.button-solid.ion-color.ion-activated) .button-native::after { - background: globals.current-color(shade); -} - -:host(.button-outline.ion-color.ion-activated) .button-native::after, -:host(.button-clear.ion-color.ion-activated) .button-native::after { - background: globals.current-color(shade, $subtle: true); -} - -:host(.ion-activated) .button-native:has(ion-ripple-effect)::after, -:host(.button-solid.ion-color.ion-activated) .button-native:has(ion-ripple-effect)::after, -:host(.button-outline.ion-color.ion-activated) .button-native:has(ion-ripple-effect)::after, -:host(.button-clear.ion-color.ion-activated) .button-native:has(ion-ripple-effect)::after { - background: transparent; -} - -// Button: Hover -// -------------------------------------------------- - -@media (any-hover: hover) { - :host(.button-solid.ion-color:hover) .button-native::after { - background: globals.current-color(shade); - } - - :host(.button-outline.ion-color:hover) .button-native::after, - :host(.button-clear.ion-color:hover) .button-native::after { - background: globals.current-color(shade, $subtle: true); - } -} - -// Button: Disabled -// -------------------------------------------------- - -:host(.button-disabled) .button-native::after { - @include globals.disabled-state(); - - width: inherit; - height: inherit; -} - -// Button Icons -// -------------------------------------------------- - -::slotted(ion-icon) { - font-size: globals.$ion-font-size-500; -} - -:host(.button-small) ::slotted(ion-icon[slot="start"]), -:host(.button-small) ::slotted(ion-spinner[slot="start"]) { - @include globals.margin-horizontal(null, globals.$ion-space-200); -} - -::slotted(ion-icon[slot="start"]), -::slotted(ion-spinner[slot="start"]) { - @include globals.margin-horizontal(null, globals.$ion-space-250); -} - -:host(.button-large) ::slotted(ion-icon[slot="start"]), -:host(.button-large) ::slotted(ion-spinner[slot="start"]) { - @include globals.margin-horizontal(null, globals.$ion-space-300); -} - -:host(.button-small) ::slotted(ion-icon[slot="end"]), -:host(.button-small) ::slotted(ion-spinner[slot="end"]) { - @include globals.margin-horizontal(globals.$ion-space-200, null); -} - -::slotted(ion-icon[slot="end"]), -::slotted(ion-spinner[slot="end"]) { - @include globals.margin-horizontal(globals.$ion-space-250, null); -} - -:host(.button-large) ::slotted(ion-icon[slot="end"]), -:host(.button-large) ::slotted(ion-spinner[slot="end"]) { - @include globals.margin-horizontal(globals.$ion-space-300, null); -} - -// Button with Badge -// -------------------------------------------------- - -:host(.button-has-badge) { - --padding-top: #{globals.$ion-space-0}; - --padding-bottom: #{globals.$ion-space-0}; -} - -// Button in Datetime -// -------------------------------------------------- - -:host(.in-datetime) { - @include globals.typography(globals.$ion-body-action-md); - - min-height: globals.$ion-space-1200; -} diff --git a/core/src/components/button/button.ios.scss b/core/src/components/button/button.ios.scss deleted file mode 100644 index 8ff37beca70..00000000000 --- a/core/src/components/button/button.ios.scss +++ /dev/null @@ -1,308 +0,0 @@ -@import "./button.native"; -@import "./button.ios.vars"; -@import "../toolbar/toolbar.ios.vars"; - -// iOS Button -// -------------------------------------------------- - -:host { - --padding-top: #{$button-ios-padding-top}; - --padding-bottom: #{$button-ios-padding-bottom}; - --padding-start: #{$button-ios-padding-start}; - --padding-end: #{$button-ios-padding-end}; - --transition: background-color, opacity 100ms linear; - - @include margin($button-ios-margin-top, $button-ios-margin-end, $button-ios-margin-bottom, $button-ios-margin-start); - - min-height: #{$button-ios-min-height}; - - font-size: #{$button-ios-font-size}; - font-weight: #{$button-ios-font-weight}; - - letter-spacing: #{$button-ios-letter-spacing}; -} - -// Solid Button -// -------------------------------------------------- - -:host(.button-solid) { - --background-activated: #{ion-color(primary, shade)}; - --background-focused: #{ion-color(primary, shade)}; - --background-hover: #{ion-color(primary, tint)}; - --background-activated-opacity: 1; - --background-focused-opacity: 1; - --background-hover-opacity: 1; -} - -// Outline Button -// -------------------------------------------------- - -:host(.button-outline) { - --border-width: #{$button-ios-outline-border-width}; - --border-style: #{$button-ios-outline-border-style}; - --background-activated: #{ion-color(primary, base)}; - --background-focused: #{ion-color(primary, base)}; - --background-hover: transparent; - --background-focused-opacity: 0.1; - --color-activated: #{ion-color(primary, contrast)}; -} - -// Clear Button -// -------------------------------------------------- - -:host(.button-clear) { - --background-activated: transparent; - --background-activated-opacity: 0; - --background-focused: #{ion-color(primary, base)}; - --background-hover: transparent; - --background-focused-opacity: 0.1; - - font-size: #{$button-ios-clear-font-size}; - font-weight: #{$button-ios-clear-font-weight}; -} - -// Toolbar Buttons -// -------------------------------------------------- - -/** - * The default buttons in a toolbar - * have a different font size/weight - * than buttons outside of a toolbar on iOS. - * However, we still want the "size"/"strong" - * properties to be usable in a toolbar, so we add - * the .in-buttons selector such that we - * can add the different font size/weight in a toolbar - * but still let "size"/"strong" override it. - */ -:host(.in-buttons) { - font-size: #{$toolbar-ios-button-font-size}; - font-weight: 400; -} - -// Button Sizes -// -------------------------------------------------- - -:host(.button-large) { - --padding-top: #{$button-ios-large-padding-top}; - --padding-start: #{$button-ios-large-padding-start}; - --padding-end: #{$button-ios-large-padding-end}; - --padding-bottom: #{$button-ios-large-padding-bottom}; - - min-height: #{$button-ios-large-min-height}; - - font-size: #{$button-ios-large-font-size}; -} - -:host(.button-small) { - --padding-top: #{$button-ios-small-padding-top}; - --padding-start: #{$button-ios-small-padding-start}; - --padding-end: #{$button-ios-small-padding-end}; - --padding-bottom: #{$button-ios-small-padding-bottom}; - - min-height: #{$button-ios-small-min-height}; - - font-size: #{$button-ios-small-font-size}; -} - -// Button Shapes -// -------------------------------------------------- - -:host(.button-soft) { - --border-radius: 6px; -} - -:host(.button-soft.button-large) { - --border-radius: 8px; -} - -:host(.button-soft.button-small) { - --border-radius: 4px; -} - -:host(.button-round) { - --border-radius: 999px; -} - -:host(.button-rectangular) { - --border-radius: 0px; -} - -// Strong Button -// -------------------------------------------------- - -:host(.button-strong) { - font-weight: #{$button-ios-strong-font-weight}; -} - -// Icon Only Button -// -------------------------------------------------- - -:host(.button-has-icon-only) { - --padding-top: 0; - --padding-bottom: var(--padding-top); - --padding-end: var(--padding-top); - --padding-start: var(--padding-end); - // TODO(FW-6053): replace em value with the min-height variable. - min-width: clamp(30px, 2.125em, 60px); - - // TODO(FW-6053): replace em value with the min-height variable. - min-height: clamp(30px, 2.125em, 60px); -} - -::slotted(ion-icon[slot="icon-only"]) { - /** - * The values were provided through a native iOS app. - * min font size: 15px, default font size: 18px, max font size: 43px - * - * Since the `ion-button` uses `rem` for the font size, we can't - * just pass the desired icon font size in `em` to the ` - * dynamic-font-clamp`. Instead, we need to adjust the base size - * to account for the ion-button` font size. - * - * For example, if the default font size of `ion-button` is 16px - * (derived from rem), then the base size can use the default font - * size of the icon. - */ - font-size: dynamic-font-clamp(0.84, 18px, 2.39, 1em); -} - -:host(.button-small.button-has-icon-only) { - // TODO(FW-6053): replace em value with the min-height variable. - min-width: clamp(23px, 2.16em, 54px); - // TODO(FW-6053): replace em value with the min-height variable. - min-height: clamp(23px, 2.16em, 54px); -} - -:host(.button-small) ::slotted(ion-icon[slot="icon-only"]) { - /** - * The values were provided through a native iOS app. - * min font size: 12px, default font size: 17px, max font size: 40px - * - * Since the `ion-button` uses `rem` for the font size, we can't - * just pass the desired icon font size in `em` to the ` - * dynamic-font-clamp`. Instead, we need to adjust the base size - * to account for the ion-button` font size. - * - * For example, if the default font size of `ion-button` is 13px - * (derived from rem) and the default font size of the icon is - * 17px, then the base size would need to be increased. - */ - font-size: dynamic-font-clamp(0.58, 20.93px, 1.92, 1em); -} - -:host(.button-large.button-has-icon-only) { - // TODO(FW-6053): replace em value with the min-height variable. - min-width: clamp(46px, 2.5em, 78px); - // TODO(FW-6053): replace em value with the min-height variable. - min-height: clamp(46px, 2.5em, 78px); -} - -:host(.button-large) ::slotted(ion-icon[slot="icon-only"]) { - /** - * The values were provided through a native iOS app. - * min font size: 15px, default font size: 18px, max font size: 43px - * - * Since the `ion-button` uses `rem` for the font size, we can't - * just pass the desired icon font size in `em` to the ` - * dynamic-font-clamp`. Instead, we need to adjust the base size - * to account for the ion-button` font size. - * - * For example, if the default font size of `ion-button` is 20px - * (derived from rem) and the default font size of the icon is - * 18px, then the base size would need to be decreased. - */ - font-size: dynamic-font-clamp(1.05, 14.4px, 2.99, 1em); -} - -// iOS Button Focused -// -------------------------------------------------- - -:host(.button-outline.ion-focused.ion-color) .button-native, -:host(.button-clear.ion-focused.ion-color) .button-native { - color: current-color(base); - - &::after { - background: current-color(base); - } -} - -:host(.button-solid.ion-color.ion-focused) .button-native::after { - background: #{current-color(shade)}; -} - -// iOS Button Hover -// -------------------------------------------------- - -@media (any-hover: hover) { - // Clear and outline buttons use opacity so set - // background to transparent, but this shouldn't - // apply on top of activated - :host(.button-clear:not(.ion-activated):hover), - :host(.button-outline:not(.ion-activated):hover) { - opacity: #{$button-ios-clear-opacity-hover}; - } - - :host(.button-clear.ion-color:hover) .button-native, - :host(.button-outline.ion-color:hover) .button-native { - color: #{current-color(base)}; - - &::after { - background: transparent; - } - } - - // Solid buttons use the tint background - :host(.button-solid.ion-color:hover) .button-native::after { - background: #{current-color(tint)}; - } - - // Solid buttons inside of a toolbar should use a tint of the current - // background so use white to tint it, but this should not apply - // when activated - :host(:hover.button-solid.in-toolbar:not(.ion-color):not(.in-toolbar-color):not(.ion-activated)) - .button-native::after { - background: #fff; - - opacity: 0.1; - } -} - -// iOS Button Activated -// -------------------------------------------------- - -:host(.button-clear.ion-activated) { - opacity: #{$button-ios-clear-opacity-activated}; -} - -:host(.button-outline.ion-activated.ion-color) .button-native { - color: current-color(contrast); - - &::after { - background: current-color(base); - } -} - -:host(.button-solid.ion-color.ion-activated) .button-native::after { - background: #{current-color(shade)}; -} - -// Activated Button in Toolbar -// -------------------------------------------------- - -:host(.button-outline.ion-activated.in-toolbar:not(.ion-color):not(.in-toolbar-color)) .button-native { - background: var(--ion-toolbar-color, var(--color)); - color: #{var(--ion-toolbar-background, var(--background), ion-color(primary, contrast))}; -} - -// Button in Datetime -// -------------------------------------------------- - -:host(.in-datetime) { - --padding-top: 3px; - --padding-bottom: 3px; - --padding-start: 5px; - --padding-end: 5px; - - font-size: dynamic-font-clamp(1, 17px, 1.24); - font-weight: 400; -} diff --git a/core/src/components/button/button.ios.vars.scss b/core/src/components/button/button.ios.vars.scss deleted file mode 100644 index ff0911a634d..00000000000 --- a/core/src/components/button/button.ios.vars.scss +++ /dev/null @@ -1,120 +0,0 @@ -@import "../../themes/native/native.globals.ios"; - -// iOS Button -// -------------------------------------------------- - -/// @prop - Margin top of the button -$button-ios-margin-top: 4px; - -/// @prop - Margin end of the button -$button-ios-margin-end: 2px; - -/// @prop - Margin bottom of the button -$button-ios-margin-bottom: 4px; - -/// @prop - Margin start of the button -$button-ios-margin-start: 2px; - -/// @prop - Padding top of the button -$button-ios-padding-top: 13px; - -/// @prop - Padding end of the button -$button-ios-padding-end: 1em; - -/// @prop - Padding bottom of the button -$button-ios-padding-bottom: $button-ios-padding-top; - -/// @prop - Padding start of the button -$button-ios-padding-start: $button-ios-padding-end; - -/// @prop - Minimum height of the button -$button-ios-min-height: 3.1em; - -/// @prop - Font size of the button text -/// The maximum font size is calculated by taking the default font size -/// and multiplying it by 3, since 310% of the default is the maximum -$button-ios-font-size: dynamic-font-max(16px, 3); - -/// @prop - Font weight of the button text -$button-ios-font-weight: 500; - -// iOS Large Button -// -------------------------------------------------- - -/// @prop - Padding top of the large button -$button-ios-large-padding-top: 17px; - -/// @prop - Padding end of the large button -$button-ios-large-padding-end: 1em; - -/// @prop - Padding bottom of the large button -$button-ios-large-padding-bottom: $button-ios-large-padding-top; - -/// @prop - Padding start of the large button -$button-ios-large-padding-start: $button-ios-large-padding-end; - -/// @prop - Minimum height of the large button -$button-ios-large-min-height: 3.1em; - -/// @prop - Font size of the large button -/// The maximum font size is calculated by taking the default font size -/// and multiplying it by 3, since 310% of the default is the maximum -$button-ios-large-font-size: dynamic-font-max(20px, 3); - -// iOS Small Button -// -------------------------------------------------- - -/// @prop - Padding top of the small button -$button-ios-small-padding-top: 4px; - -/// @prop - Padding end of the small button -$button-ios-small-padding-end: 0.9em; - -/// @prop - Padding bottom of the small button -$button-ios-small-padding-bottom: $button-ios-small-padding-top; - -/// @prop - Padding start of the small button -$button-ios-small-padding-start: $button-ios-small-padding-end; - -/// @prop - Minimum height of the small button -$button-ios-small-min-height: 2.1em; - -/// @prop - Font size of the small button -/// The maximum font size is calculated by taking the default font size -/// and multiplying it by 3, since 310% of the default is the maximum -$button-ios-small-font-size: dynamic-font-max(13px, 3); - -// iOS Outline Button -// -------------------------------------------------- - -/// @prop - Border width of the outline button -$button-ios-outline-border-width: 1px; - -/// @prop - Border style of the outline button -$button-ios-outline-border-style: solid; - -// iOS Clear Button -// -------------------------------------------------- - -/// @prop - Font size of the clear button -/// The maximum font size is calculated by taking the default font size -/// and multiplying it by 3, since 310% of the default is the maximum -$button-ios-clear-font-size: dynamic-font-max(17px, 3); - -/// @prop - Font weight of the clear button -$button-ios-clear-font-weight: normal; - -/// @prop - Letter spacing of the button -$button-ios-letter-spacing: 0; - -/// @prop - Opacity of the activated clear button -$button-ios-clear-opacity-activated: 0.4; - -/// @prop - Opacity of the clear button on hover -$button-ios-clear-opacity-hover: 0.6; - -// iOS Decorator Button -// -------------------------------------------------- - -/// @prop - Font weight of the strong button -$button-ios-strong-font-weight: 600; diff --git a/core/src/components/button/button.md.scss b/core/src/components/button/button.md.scss deleted file mode 100644 index 35fd00dbce1..00000000000 --- a/core/src/components/button/button.md.scss +++ /dev/null @@ -1,262 +0,0 @@ -@import "./button.native"; -@import "./button.md.vars"; - -// Material Design Button -// -------------------------------------------------- - -:host { - --padding-top: #{$button-md-padding-top}; - --padding-bottom: #{$button-md-padding-bottom}; - --padding-start: #{$button-md-padding-start}; - --padding-end: #{$button-md-padding-end}; - --transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1), background-color 15ms linear, color 15ms linear; - - @include margin($button-md-margin-top, $button-md-margin-end, $button-md-margin-bottom, $button-md-margin-start); - - min-height: #{$button-md-min-height}; - - font-size: #{$button-md-font-size}; - font-weight: #{$button-md-font-weight}; - - letter-spacing: #{$button-md-letter-spacing}; - - text-transform: #{$button-md-text-transform}; -} - -// Solid Button -// -------------------------------------------------- - -:host(.button-solid) { - --background-activated: transparent; - --background-hover: #{ion-color(primary, contrast)}; - --background-focused: #{ion-color(primary, contrast)}; - --background-activated-opacity: 0; - --background-focused-opacity: 0.24; - --background-hover-opacity: 0.08; - --box-shadow: #{$button-md-box-shadow}; -} - -:host(.button-solid.ion-activated) { - --box-shadow: #{$button-md-box-shadow-activated}; -} - -// Outline Button -// -------------------------------------------------- - -:host(.button-outline) { - --border-width: 2px; - --border-style: solid; - --box-shadow: none; - --background-activated: transparent; - --background-focused: #{ion-color(primary, base)}; - --background-hover: #{ion-color(primary, base)}; - --background-activated-opacity: 0; - --background-focused-opacity: 0.12; - --background-hover-opacity: 0.04; -} - -:host(.button-outline.ion-activated.ion-color) .button-native { - background: transparent; -} - -// Clear Button -// -------------------------------------------------- - -:host(.button-clear) { - --background-activated: transparent; - --background-focused: #{ion-color(primary, base)}; - --background-hover: #{ion-color(primary, base)}; - --background-activated-opacity: 0; - --background-focused-opacity: 0.12; - --background-hover-opacity: 0.04; -} - -// Button Shapes -// ------------------------------------------------------------------------------- - -:host(.button-soft) { - --border-radius: 4px; -} - -:host(.button-round) { - --border-radius: 999px; -} - -:host(.button-rectangular) { - --border-radius: 0px; -} - -// Button Sizes -// -------------------------------------------------- - -:host(.button-large) { - --padding-top: #{$button-md-large-padding-top}; - --padding-start: #{$button-md-large-padding-start}; - --padding-end: #{$button-md-large-padding-end}; - --padding-bottom: #{$button-md-large-padding-bottom}; - - min-height: #{$button-md-large-min-height}; - - font-size: #{$button-md-large-font-size}; -} - -:host(.button-small) { - --padding-top: #{$button-md-small-padding-top}; - --padding-start: #{$button-md-small-padding-start}; - --padding-end: #{$button-md-small-padding-end}; - --padding-bottom: #{$button-md-small-padding-bottom}; - - min-height: #{$button-md-small-min-height}; - - font-size: #{$button-md-small-font-size}; -} - -// Strong Button -// -------------------------------------------------- - -:host(.button-strong) { - font-weight: #{$button-md-strong-font-weight}; -} - -// Icon Only Button -// -// MD does not specify a small size, these -// styles are based on the iOS small size. -// -// MD does not specify a large size, these -// styles are based on the iOS large size. -// -------------------------------------------------- - -:host(.button-has-icon-only) { - --padding-top: 0; - --padding-bottom: var(--padding-top); - --padding-end: var(--padding-top); - --padding-start: var(--padding-end); - // TODO(FW-6053): replace em value with the min-height variable. - min-width: clamp(30px, 2.86em, 60px); - - // TODO(FW-6053): replace em value with the min-height variable. - min-height: clamp(30px, 2.86em, 60px); -} - -::slotted(ion-icon[slot="icon-only"]) { - /** - * The values were provided through MD design, - * large and small are based on the iOS sizes. - * min font size: 15px, default font size: 22.4px, max font size: 43px - * - * Since the `ion-button` uses `rem` for the font size, we can't - * just pass the desired icon font size in `em` to the ` - * dynamic-font-clamp`. Instead, we need to adjust the base size - * to account for the ion-button` font size. - * - * For example, if the default font size of `ion-button` is 14px - * (derived from rem) and the default font size of the icon is - * 22.4px, then the base size would need to be increased. - */ - font-size: dynamic-font-clamp(0.59, 25.6px, 1.68, 1em); -} - -:host(.button-small.button-has-icon-only) { - // TODO(FW-6053): replace em value with the min-height variable. - min-width: clamp(23px, 2.16em, 54px); - // TODO(FW-6053): replace em value with the min-height variable. - min-height: clamp(23px, 2.16em, 54px); -} - -:host(.button-small) ::slotted(ion-icon[slot="icon-only"]) { - /** - * The values were provided through MD design, - * large and small are based on the iOS sizes. - * min font size: 12px, default font size: 16px, max font size: 40px - * - * Since the `ion-button` uses `rem` for the font size, we can't - * just pass the desired icon font size in `em` to the ` - * dynamic-font-clamp`. Instead, we need to adjust the base size - * to account for the ion-button` font size. - * - * For example, if the default font size of `ion-button` is 13px - * (derived from rem) and the default font size of the icon is - * 16px, then the base size would need to be increased. - */ - font-size: dynamic-font-clamp(0.66, 19.7px, 2.05, 1em); -} - -:host(.button-large.button-has-icon-only) { - // TODO(FW-6053): replace em value with the min-height variable. - min-width: clamp(46px, 2.5em, 78px); - // TODO(FW-6053): replace em value with the min-height variable. - min-height: clamp(46px, 2.5em, 78px); -} - -:host(.button-large) ::slotted(ion-icon[slot="icon-only"]) { - /** - * The values were provided through MD design, - * large and small are based on the iOS sizes. - * min font size: 15px, default font size: 28px, max font size: 43px - * - * Since the `ion-button` uses `rem` for the font size, we can't - * just pass the desired icon font size in `em` to the ` - * dynamic-font-clamp`. Instead, we need to adjust the base size - * to account for the ion-button` font size. - * - * For example, if the default font size of `ion-button` is 20px - * (derived from rem) and the default font size of the icon is - * 28px, then the base size would need to be increased. - */ - font-size: dynamic-font-clamp(0.67, 22.4px, 1.92, 1em); -} - -// Button: Hover -// -------------------------------------------------- - -/** - * Only allow overriding of opacity here - * as developers should not be overriding - * colors when using the color prop. - */ - -:host(.button-solid.ion-color.ion-focused) .button-native::after { - background: #{current-color(contrast)}; -} - -:host(.button-clear.ion-color.ion-focused) .button-native::after, -:host(.button-outline.ion-color.ion-focused) .button-native::after { - background: #{current-color(base)}; -} - -@media (any-hover: hover) { - :host(.button-solid.ion-color:hover) .button-native::after { - background: #{current-color(contrast)}; - } - - :host(.button-clear.ion-color:hover) .button-native::after, - :host(.button-outline.ion-color:hover) .button-native::after { - background: #{current-color(base)}; - } -} - -// Activated Button in Toolbar -// -------------------------------------------------- - -:host(.button-outline.ion-activated.in-toolbar:not(.ion-color):not(.in-toolbar-color)) .button-native { - background: var(--ion-toolbar-background, var(--color)); - color: #{var(--ion-toolbar-color, var(--background), ion-color(primary, contrast))}; -} - -// Button in Datetime -// -------------------------------------------------- - -:host(.in-datetime) { - --padding-top: 3px; - --padding-bottom: 3px; - --padding-start: 8px; - --padding-end: 8px; - --box-shadow: none; -} - -:host(.in-datetime.button-clear) { - --background-activated: transparent; - --background-focused: currentColor; - --background-hover: currentColor; -} diff --git a/core/src/components/button/button.md.vars.scss b/core/src/components/button/button.md.vars.scss deleted file mode 100644 index dd68ef0ebb4..00000000000 --- a/core/src/components/button/button.md.vars.scss +++ /dev/null @@ -1,98 +0,0 @@ -@import "../../themes/native/native.globals.md"; - -// Material Design Button -// -------------------------------------------------- - -/// @prop - Margin top of the button -$button-md-margin-top: 4px; - -/// @prop - Margin end of the button -$button-md-margin-end: 2px; - -/// @prop - Margin bottom of the button -$button-md-margin-bottom: 4px; - -/// @prop - Margin start of the button -$button-md-margin-start: 2px; - -/// @prop - Padding top of the button -$button-md-padding-top: 8px; - -/// @prop - Padding end of the button -$button-md-padding-end: 1.1em; - -/// @prop - Padding bottom of the button -$button-md-padding-bottom: $button-md-padding-top; - -/// @prop - Padding start of the button -$button-md-padding-start: 1.1em; - -/// @prop - Minimum height of the button -$button-md-min-height: 36px; - -/// @prop - Font size of the button text -$button-md-font-size: dynamic-font(14px); - -/// @prop - Font weight of the button text -$button-md-font-weight: 500; - -/// @prop - Capitalization of the button text -$button-md-text-transform: uppercase; - -$button-md-letter-spacing: 0.06em; - -/// @prop - Box shadow of the button -$button-md-box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), - 0 1px 5px 0 rgba(0, 0, 0, 0.12); - -/// @prop - Box shadow of the activated button -$button-md-box-shadow-activated: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), - 0 3px 14px 2px rgba(0, 0, 0, 0.12); - -// Material Design Large Button -// -------------------------------------------------- - -/// @prop - Padding top of the large button -$button-md-large-padding-top: 14px; - -/// @prop - Padding end of the large button -$button-md-large-padding-end: 1em; - -/// @prop - Padding bottom of the large button -$button-md-large-padding-bottom: $button-md-large-padding-top; - -/// @prop - Padding start of the large button -$button-md-large-padding-start: $button-md-large-padding-end; - -/// @prop - Minimum height of the large button -$button-md-large-min-height: 2.8em; - -/// @prop - Font size of the large button -$button-md-large-font-size: dynamic-font(20px); - -// Material Design Small Button -// -------------------------------------------------- - -/// @prop - Padding top of the small button -$button-md-small-padding-top: 4px; - -/// @prop - Padding end of the small button -$button-md-small-padding-end: 0.9em; - -/// @prop - Padding bottom of the small button -$button-md-small-padding-bottom: $button-md-small-padding-top; - -/// @prop - Padding start of the small button -$button-md-small-padding-start: $button-md-small-padding-end; - -/// @prop - Minimum height of the small button -$button-md-small-min-height: 2.1em; - -/// @prop - Font size of the small button -$button-md-small-font-size: dynamic-font(13px); - -// Material Design Decorator Button -// -------------------------------------------------- - -/// @prop - Font weight of the strong button -$button-md-strong-font-weight: bold; diff --git a/core/src/components/button/button.native.scss b/core/src/components/button/button.native.scss deleted file mode 100644 index 07a32238f20..00000000000 --- a/core/src/components/button/button.native.scss +++ /dev/null @@ -1,59 +0,0 @@ -@use "../../themes/native/native.globals" as globals; -@use "./button.common"; - -// Button - iOS and Material Design -// -------------------------------------------------- - -:host { - font-family: globals.$font-family-base; -} - -// Solid Button -// -------------------------------------------------- - -// Default Solid Color -:host(.button-solid) { - --background: #{globals.ion-color(primary, base)}; - --color: #{globals.ion-color(primary, contrast)}; -} - -// Outline Button -// -------------------------------------------------- - -// Default Outline Color -:host(.button-outline) { - --border-color: #{globals.ion-color(primary, base)}; - --color: #{globals.ion-color(primary, base)}; -} - -// Clear Button -// -------------------------------------------------- - -// Default Clear Color -:host(.button-clear) { - --color: #{globals.ion-color(primary, foreground)}; -} - -// Button Icons -// -------------------------------------------------- - -::slotted(ion-icon) { - font-size: 1.35em; -} - -::slotted(ion-icon[slot="start"]) { - @include globals.margin(0, 0.3em, 0, -0.3em); -} - -::slotted(ion-icon[slot="end"]) { - @include globals.margin(0, -0.2em, 0, 0.3em); -} - -// Button in Datetime -// -------------------------------------------------- - -:host(.in-datetime) { - @include globals.margin(0px, 2px, 0px, 2px); - - min-height: 32px; -} diff --git a/core/src/components/button/button.scss b/core/src/components/button/button.scss new file mode 100644 index 00000000000..ff68a70b6b4 --- /dev/null +++ b/core/src/components/button/button.scss @@ -0,0 +1,472 @@ +@use "../../themes/mixins" as mixins; +@use "./button.docs"; + +// Button +// -------------------------------------------------- + +$fills: solid, outline, clear; +$sizes: small, medium, large; +$shapes: crisp, soft, round, rectangular; +$spinner-sizes: xsmall, small, medium, large, xlarge; + +:host { + /** + * The undocumented padding hooks still work for the `ion-padding` classes set on + * the button itself. Reset them so the button never inherits them from an ancestor. + * + * TODO(FW-6838): Parents such as ion-buttons, ion-item and ion-input still set these + * hooks on buttons. Move them to nested tokens, such as `IonItem.IonButton.size.small.padding`. + */ + --padding-top: initial; + --padding-end: initial; + --padding-bottom: initial; + --padding-start: initial; + + @include mixins.margin( + var(--ion-button-margin-top), + var(--ion-button-margin-end), + var(--ion-button-margin-bottom), + var(--ion-button-margin-start) + ); + + display: inline-block; + + width: auto; + + font-family: var(--ion-button-font-family, inherit); + + letter-spacing: var(--ion-button-letter-spacing); + + text-align: center; + text-decoration: none; + text-transform: var(--ion-button-text-transform); + + white-space: normal; + + user-select: none; + + // Firefox doesn't support the value below, so it drops that declaration and keeps this one + vertical-align: top; + + /** + * Non-standard, supported by Chromium and Safari. It centers the button on the + * text baseline, so a button inline with text lines up with it. + */ + vertical-align: -webkit-baseline-middle; + + font-kerning: none; +} + +// Button: Native +// -------------------------------------------------- + +.button-native { + @include mixins.font-smoothing(); + @include mixins.margin(0); + @include mixins.text-inherit(); + + display: flex; + + position: relative; + + align-items: center; + + width: 100%; + height: 100%; + + min-height: inherit; + + transition: var(--ion-button-transition); + + outline: none; + + line-height: 1; + + contain: layout style; + cursor: pointer; + + overflow: hidden; + + z-index: 0; + box-sizing: border-box; + appearance: none; +} + +.button-native::-moz-focus-inner { + border: 0; +} + +.button-inner { + display: flex; + position: relative; + + flex-flow: row nowrap; + flex-shrink: 0; + align-items: center; + justify-content: center; + + width: 100%; + height: 100%; + + z-index: 1; +} + +// Button: Sizes +// -------------------------------------------------- + +@each $size in $sizes { + :host(.button-size-#{$size}) { + min-width: var(--ion-button-size-#{$size}-min-width); + min-height: var(--ion-button-size-#{$size}-min-height); + + font-size: var(--ion-button-size-#{$size}-font-size); + + line-height: var(--ion-button-size-#{$size}-line-height); + } + + :host(.button-size-#{$size}) .button-native { + @include mixins.padding( + var(--padding-top, var(--ion-button-size-#{$size}-padding-top)), + var(--padding-end, var(--ion-button-size-#{$size}-padding-end)), + var(--padding-bottom, var(--ion-button-size-#{$size}-padding-bottom)), + var(--padding-start, var(--ion-button-size-#{$size}-padding-start)) + ); + } +} + +// Button: Fill Typography +// -------------------------------------------------- + +@each $fill in $fills { + :host(.button-hue-bold.button-fill-#{$fill}) { + font-weight: var(--ion-button-hue-bold-#{$fill}-default-font-weight); + } +} + +@each $fill in $fills { + :host(.button-hue-bold.button-fill-#{$fill}.button-strong) { + font-weight: var(--ion-button-hue-bold-#{$fill}-strong-font-weight); + } +} + +// Button: Shapes +// -------------------------------------------------- + +@each $shape in $shapes { + :host(.button-shape-#{$shape}) .button-native { + @include mixins.border-radius(var(--ion-button-shape-#{$shape}-border-radius)); + } +} + +.button-native ion-ripple-effect { + @include mixins.border-radius(inherit); +} + +// Button: Icon Only +// -------------------------------------------------- + +@each $size in $sizes { + // A square as tall as a text button of the same size. Not `aspect-ratio`, + // which needs a newer Firefox than Ionic supports. + :host(.button-size-#{$size}.button-has-icon-only) { + min-width: var(--ion-button-size-#{$size}-min-height); + min-height: var(--ion-button-size-#{$size}-min-height); + } + + :host(.button-size-#{$size}.button-has-icon-only) .button-native { + @include mixins.padding( + var(--padding-top, var(--ion-button-size-#{$size}-icon-only-padding-top)), + var(--padding-end, var(--ion-button-size-#{$size}-icon-only-padding-end)), + var(--padding-bottom, var(--ion-button-size-#{$size}-icon-only-padding-bottom)), + var(--padding-start, var(--ion-button-size-#{$size}-icon-only-padding-start)) + ); + } +} + +// Button: Slotted Elements +// -------------------------------------------------- + +::slotted([slot="start"]), +::slotted([slot="end"]) { + flex-shrink: 0; +} + +::slotted(ion-icon), +::slotted(ion-spinner) { + pointer-events: none; +} + +::slotted(ion-spinner) { + color: currentColor; +} + +/** + * Only the ionic theme sizes and spaces a slotted spinner. The other + * themes fall back to the spinner's own size and no margin. + */ +@each $size in $sizes { + @each $spinner-size in $spinner-sizes { + $spinner: "ion-spinner.spinner-size-#{$spinner-size}"; + + :host(.button-size-#{$size}) ::slotted(#{$spinner}[slot="start"]), + :host(.button-size-#{$size}) ::slotted(#{$spinner}[slot="end"]), + :host(.button-size-#{$size}) ::slotted(#{$spinner}[slot="icon-only"]) { + $token: --ion-button-size-#{$size}-spinner-size-#{$spinner-size}; + + width: var(#{$token}-width, var(--ion-spinner-size-#{$spinner-size}-width)); + height: var(#{$token}-height, var(--ion-spinner-size-#{$spinner-size}-height)); + } + } +} + +@each $size in $sizes { + :host(.button-size-#{$size}) ::slotted(ion-icon) { + font-size: var(--ion-button-size-#{$size}-icon-font-size); + } + + :host(.button-size-#{$size}) ::slotted(ion-icon[slot="icon-only"]) { + font-size: var(--ion-button-size-#{$size}-icon-only-icon-font-size); + } +} + +@each $size in $sizes { + :host(.button-size-#{$size}) ::slotted(ion-icon[slot="start"]) { + @include mixins.margin( + var(--ion-button-size-#{$size}-icon-leading-margin-top), + var(--ion-button-size-#{$size}-icon-leading-margin-end), + var(--ion-button-size-#{$size}-icon-leading-margin-bottom), + var(--ion-button-size-#{$size}-icon-leading-margin-start) + ); + } + + :host(.button-size-#{$size}) ::slotted(ion-icon[slot="end"]) { + @include mixins.margin( + var(--ion-button-size-#{$size}-icon-trailing-margin-top), + var(--ion-button-size-#{$size}-icon-trailing-margin-end), + var(--ion-button-size-#{$size}-icon-trailing-margin-bottom), + var(--ion-button-size-#{$size}-icon-trailing-margin-start) + ); + } + + :host(.button-size-#{$size}) ::slotted(ion-spinner[slot="start"]) { + @include mixins.margin( + var(--ion-button-size-#{$size}-spinner-leading-margin-top), + var(--ion-button-size-#{$size}-spinner-leading-margin-end), + var(--ion-button-size-#{$size}-spinner-leading-margin-bottom), + var(--ion-button-size-#{$size}-spinner-leading-margin-start) + ); + } + + :host(.button-size-#{$size}) ::slotted(ion-spinner[slot="end"]) { + @include mixins.margin( + var(--ion-button-size-#{$size}-spinner-trailing-margin-top), + var(--ion-button-size-#{$size}-spinner-trailing-margin-end), + var(--ion-button-size-#{$size}-spinner-trailing-margin-bottom), + var(--ion-button-size-#{$size}-spinner-trailing-margin-start) + ); + } +} + +// Button: Hues and Fills +// -------------------------------------------------- + +/** + * Each fill has a resting look and one for each state. Without a color, + * the button reads the plain tokens. With a color, it reads the `semantic` + * tokens, which use the color's base, contrast and so on. + */ +@each $fill in $fills { + $token: --ion-button-hue-bold-#{$fill}; + + :host(.button-hue-bold.button-fill-#{$fill}) { + color: var(#{$token}-default-color); + } + + :host(.button-hue-bold.button-fill-#{$fill}) .button-native { + border-width: var(#{$token}-default-border-width); + border-style: var(#{$token}-default-border-style); + border-color: var(#{$token}-default-border-color); + + background: var(#{$token}-default-background); + + box-shadow: var(#{$token}-default-box-shadow); + } + + // The ripple sits in this shadow root, out of reach of the global token CSS + :host(.button-hue-bold.button-fill-#{$fill}) ion-ripple-effect { + // A variable, because the ripple animates its opacity in a keyframe + --ion-ripple-effect-opacity: var(#{$token}-ripple-effect-opacity); + + color: var(#{$token}-ripple-effect-color); + } + + :host(.button-hue-bold.button-fill-#{$fill}.ion-color) .button-native { + border-color: var(#{$token}-semantic-default-border-color); + + background: var(#{$token}-semantic-default-background); + color: var(#{$token}-semantic-default-color); + } + + :host(.button-hue-bold.button-fill-#{$fill}.ion-color) ion-ripple-effect { + --ion-ripple-effect-opacity: var(#{$token}-semantic-ripple-effect-opacity); + + color: var(#{$token}-semantic-ripple-effect-color); + } +} + +/** + * The states are written in this order: focus -> hover -> activated -> disabled. + * Their rules share a specificity, so a later state overrides an earlier + * one: disabled overrides all, and activated overrides hover and focus. + */ + +// Focused +@each $fill in $fills { + $token: --ion-button-hue-bold-#{$fill}; + + :host(.button-hue-bold.button-fill-#{$fill}.ion-focused) { + color: var(#{$token}-focus-color); + } + + :host(.button-hue-bold.button-fill-#{$fill}.ion-focused) .button-native { + background: var(#{$token}-focus-background); + } + + :host(.button-hue-bold.button-fill-#{$fill}.ion-color.ion-focused) .button-native { + background: var(#{$token}-semantic-focus-background); + color: var(#{$token}-semantic-focus-color); + } +} + +// Hover +@media (any-hover: hover) { + @each $fill in $fills { + $token: --ion-button-hue-bold-#{$fill}; + + :host(.button-hue-bold.button-fill-#{$fill}:hover) { + color: var(#{$token}-hover-color); + } + + // Disabled keeps its own opacity + :host(.button-hue-bold.button-fill-#{$fill}:not(.button-disabled):hover) { + opacity: var(#{$token}-hover-opacity); + } + + :host(.button-hue-bold.button-fill-#{$fill}:hover) .button-native { + background: var(#{$token}-hover-background); + } + + :host(.button-hue-bold.button-fill-#{$fill}.ion-color:hover) .button-native { + background: var(#{$token}-semantic-hover-background); + color: var(#{$token}-semantic-hover-color); + } + } +} + +// Activated +@each $fill in $fills { + $token: --ion-button-hue-bold-#{$fill}; + + :host(.button-hue-bold.button-fill-#{$fill}.ion-activated) { + color: var(#{$token}-activated-color); + } + + // Disabled keeps its own opacity + :host(.button-hue-bold.button-fill-#{$fill}.ion-activated:not(.button-disabled)) { + opacity: var(#{$token}-activated-opacity); + } + + :host(.button-hue-bold.button-fill-#{$fill}.ion-activated) .button-native { + background: var(#{$token}-activated-background); + + box-shadow: var(#{$token}-activated-box-shadow); + } + + :host(.button-hue-bold.button-fill-#{$fill}.ion-color.ion-activated) .button-native { + background: var(#{$token}-semantic-activated-background); + color: var(#{$token}-semantic-activated-color); + } + + /** + * Only `md` mode renders a ripple. When it does, the ripple shows the press, + * so the button keeps its default background instead of the activated one. + */ + :host(.button-hue-bold.button-fill-#{$fill}.ion-activated) .button-native:has(ion-ripple-effect) { + background: var(#{$token}-default-background); + } + + :host(.button-hue-bold.button-fill-#{$fill}.ion-color.ion-activated) .button-native:has(ion-ripple-effect) { + background: var(#{$token}-semantic-default-background); + } +} + +// Disabled +@each $fill in $fills { + $token: --ion-button-hue-bold-#{$fill}; + + :host(.button-hue-bold.button-fill-#{$fill}.button-disabled) { + opacity: var(#{$token}-disabled-opacity); + } + + :host(.button-hue-bold.button-fill-#{$fill}.button-disabled) .button-native { + background: var(#{$token}-disabled-background); + } + + :host(.button-hue-bold.button-fill-#{$fill}.ion-color.button-disabled) .button-native { + background: var(#{$token}-semantic-disabled-background); + } +} + +// Button: Disabled +// -------------------------------------------------- + +:host(.button-disabled) { + cursor: default; + pointer-events: none; +} + +// Button: Focus Ring +// -------------------------------------------------- + +:host(.ion-focused) .button-native { + outline-color: var(--ion-button-state-focus-ring-color); + outline-offset: var(--ion-button-state-focus-ring-offset); + outline-style: var(--ion-button-state-focus-ring-style); + outline-width: var(--ion-button-state-focus-ring-width); +} + +// Button: Expand +// -------------------------------------------------- + +// After the fills, so a full button drops the side borders they set +:host(.button-expand-block), +:host(.button-expand-full) { + display: block; +} + +:host(.button-expand-block) .button-native, +:host(.button-expand-full) .button-native { + @include mixins.margin-horizontal(0); + + width: 100%; + + contain: content; +} + +:host(.button-expand-block) .button-native { + clear: both; +} + +:host(.button-expand-full:not(.button-shape-round)) .button-native { + @include mixins.border-radius(0); + + border-right-width: 0; + border-left-width: 0; +} + +// Button: With a Badge +// -------------------------------------------------- + +:host(.button-has-badge) .button-native { + overflow: visible; +} diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx index 7b584e205b4..e3f1b33aac8 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -8,13 +8,16 @@ import { hasShadowDom, openURL } from '@utils/helpers'; import { printIonWarning } from '@utils/logging'; import { createColorClasses, hostContext } from '@utils/theme'; -import { getIonTheme, getIonMode } from '../../global/ionic-global'; +import { config } from '../../global/config'; +import { getIonMode } from '../../global/ionic-global'; import type { AnimationBuilder, Color } from '../../interface'; +import type { Hue } from '../../themes/themes.interfaces'; import type { RouterDirection } from '../router/utils/interface'; +import type { IonButtonExpand, IonButtonFill, IonButtonShape, IonButtonSize } from './button.interface'; + /** * @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component. - * @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component. * * @slot - Content is placed between the named slots if provided without a slot. * @slot icon-only - Should be used on an icon in a button that has no text. @@ -25,17 +28,14 @@ import type { RouterDirection } from '../router/utils/interface'; */ @Component({ tag: 'ion-button', - styleUrls: { - ios: 'button.ios.scss', - md: 'button.md.scss', - ionic: 'button.ionic.scss', - }, + styleUrl: 'button.scss', shadow: true, }) export class Button implements ComponentInterface, AnchorInterface, ButtonInterface { private hasLoaded = false; private inDatetime = false; private inItem = false; + private hasWarnedSizeDefault = false; private inListHeader = false; private inButtons = false; private formButtonEl: HTMLButtonElement | null = null; @@ -83,15 +83,17 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf * Set to `"block"` for a full-width button or to `"full"` for a full-width button * with square corners and no left or right borders. */ - @Prop({ reflect: true }) expand?: 'full' | 'block'; + @Prop({ reflect: true }) expand?: IonButtonExpand; /** - * Set to `"clear"` for a transparent button that resembles a flat button, to `"outline"` - * for a transparent button with a border, or to `"solid"` for a button with a filled background. - * The default fill is `"solid"` except when inside of a buttons or datetime component, where - * the default fill is `"clear"`. + * Set to `"clear"` for a transparent button that resembles a flat button, + * `"outline"` for a transparent button with a border, + * or `"solid"` for a button with a filled background. + * + * Defaults to `"solid"` if both the fill property and theme config are unset, + * or `"clear"` inside of buttons, datetime, or list header. */ - @Prop({ reflect: true, mutable: true }) fill?: 'clear' | 'outline' | 'solid' | 'default'; + @Prop({ reflect: true, mutable: true }) fill?: IonButtonFill | 'default'; /** * When using a router, it specifies the transition direction when navigating to @@ -126,20 +128,26 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf @Prop() rel: string | undefined; /** - * Set to `"soft"` for a button with slightly rounded corners, `"round"` for a button with fully - * rounded corners, or `"rectangular"` for a button without rounded corners. - * Defaults to `"soft"` for the `"ios"` theme and `"round"` for all other themes. + * Set to `"crisp"` for a button with even slightly rounded corners, + * `"soft"` for a button with slightly rounded corners, + * `"round"` for a button with fully rounded corners, + * or `"rectangular"` for a button without rounded corners. + * + * Defaults to `"round"` if both the shape property and theme config are unset. */ - @Prop({ reflect: true }) shape?: 'soft' | 'round' | 'rectangular'; + @Prop({ reflect: true }) shape?: IonButtonShape; /** - * Set to `"small"` for a button with less height and padding, to `"medium"` - * for a button with the medium height and padding, or to `"large"` for a button - * with more height and padding. By default the size is unset, unless the button - * is inside of an item, where the size is `"small"` by default. Set the size to - * `"default"` inside of an item to make it a standard size button. + * Set to `"small"` for a button with less height and padding, + * `"medium"` for a button with the standard height and padding, + * or `"large"` for a button with more height and padding. + * + * Defaults to `"medium"` if both the size property and theme config are unset, + * or `"small"` inside of an item. + * + * `"default"` is deprecated and renders the same as `"medium"`. */ - @Prop({ reflect: true }) size?: 'small' | 'default' | 'medium' | 'large'; + @Prop({ reflect: true }) size?: IonButtonSize | 'default'; /** * If `true`, activates a button with a heavier font weight. @@ -254,10 +262,10 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf } private get rippleType() { - const hasClearFill = this.fill === undefined || this.fill === 'clear'; + const hasClearFill = this.fillValue === 'clear'; // Use the unbounded "circular" ripple effect if it: - // - Has a clear fill (the default) + // - Has a clear fill (the default in buttons and datetime) // - Only has an icon and // - Is inside of buttons (used in a toolbar) or a datetime if (hasClearFill && this.hasIconOnly && (this.inButtons || this.inDatetime)) { @@ -268,28 +276,65 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf } /** - * Set the shape based on the theme + * Gets the button fill. Uses the `fill` property if set, otherwise `clear` inside of + * buttons, datetime or list header, then the theme config. Defaults to `solid` if none apply. */ - private getShape(): string { - const theme = getIonTheme(this); - const { shape } = this; + get fillValue(): IonButtonFill | 'default' { + const { fill } = this; + + if (fill !== undefined) { + return fill; + } - if (shape === undefined) { - return theme === 'ios' ? 'soft' : 'round'; + if (this.inDatetime || this.inButtons || this.inListHeader) { + return 'clear'; } - return shape; + return config.getObjectValue('IonButton.fill', 'solid') as IonButtonFill; + } + + /** + * Gets the button hue from the theme config. Defaults to `bold` if it is unset. + */ + get hueValue(): Hue { + return config.getObjectValue('IonButton.hue', 'bold') as Hue; + } + + /** + * Gets the button shape. Uses the `shape` property if set, otherwise + * checks the theme config. Defaults to `round` if neither is set. + */ + get shapeValue(): IonButtonShape { + const shapeConfig = config.getObjectValue('IonButton.shape', 'round') as IonButtonShape; + + return this.shape ?? shapeConfig; } /** - * Get the default size. If size is unset and the button is inside an item, - * the default size is "small". Otherwise, it uses the value of size. + * Gets the button size. Uses the `size` property if set, otherwise `small` + * inside of an item, then the theme config. Defaults to `medium` if none apply. */ - private getSize(): string | undefined { + get sizeValue(): IonButtonSize { const { size } = this; if (size === undefined) { - return this.inItem ? 'small' : 'medium'; + if (this.inItem) { + return 'small'; + } + + return config.getObjectValue('IonButton.size', 'medium') as IonButtonSize; + } + + if (size === 'default') { + if (!this.hasWarnedSizeDefault) { + this.hasWarnedSizeDefault = true; + printIonWarning( + '[ion-button] - The "default" size is deprecated. Use "medium" instead.\nExample: ', + this.el + ); + } + + return 'medium'; } return size; @@ -398,12 +443,24 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf }; render() { - const { buttonType, type, disabled, rel, target, href, color, expand, hasIconOnly, hasBadge, strong } = this; - - const theme = getIonTheme(this); + const { + buttonType, + type, + disabled, + rel, + target, + href, + color, + expand, + hasIconOnly, + hasBadge, + fillValue, + hueValue, + shapeValue, + sizeValue, + strong, + } = this; const mode = getIonMode(this); - const size = this.getSize(); - const shape = this.getShape(); const inheritedAttributes = this.ariaController?.attributes ?? {}; const TagType = href === undefined ? 'button' : ('a' as any); const attrs = @@ -415,10 +472,6 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf rel, target, }; - let fill = this.fill; - if (fill === undefined) { - fill = this.inDatetime || this.inButtons || this.inListHeader ? 'clear' : 'solid'; - } /** * We call renderHiddenButton in the render function to account @@ -436,12 +489,13 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf onClick={this.handleClick} aria-disabled={disabled ? 'true' : null} class={createColorClasses(color, { - [theme]: true, [buttonType]: true, - [`${buttonType}-${expand}`]: expand !== undefined, - [`${buttonType}-${size}`]: size !== undefined, - [`${buttonType}-${shape}`]: true, - [`${buttonType}-${fill}`]: true, + [`${buttonType}-expand-${expand}`]: expand !== undefined, + [`${buttonType}-size-${sizeValue}`]: true, + [`${buttonType}-shape-${shapeValue}`]: true, + [`${buttonType}-fill-${fillValue}`]: true, + // TODO(FW-6838): read the hue prop first in the hue PR + [`${buttonType}-hue-${hueValue}`]: true, [`${buttonType}-strong`]: strong, 'in-datetime': this.inDatetime, 'in-toolbar': hostContext('ion-toolbar', this.el), diff --git a/core/src/components/button/test/basic/button.e2e.ts b/core/src/components/button/test/basic/button.e2e.ts index 500bee8a3ad..f2c3f836388 100644 --- a/core/src/components/button/test/basic/button.e2e.ts +++ b/core/src/components/button/test/basic/button.e2e.ts @@ -28,12 +28,12 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ config, title }) => { ); const button = page.locator('ion-button'); - await expect(button).toHaveClass(/button-outline/); + await expect(button).toHaveClass(/button-fill-outline/); await button.evaluate((el: HTMLIonButtonElement) => (el.fill = undefined)); await page.waitForChanges(); - await expect(button).toHaveClass(/button-solid/); + await expect(button).toHaveClass(/button-fill-solid/); }); }); }); diff --git a/core/src/components/button/test/basic/index.html b/core/src/components/button/test/basic/index.html index 19efe474cea..9e56c946988 100644 --- a/core/src/components/button/test/basic/index.html +++ b/core/src/components/button/test/basic/index.html @@ -84,13 +84,13 @@

- Opacity: 0.2 + Opacity: 0.2

Button Disabled Secondary Disabled - Disabled opacity: 1 + Tertiary Disabled

diff --git a/core/src/components/button/test/button.spec.ts b/core/src/components/button/test/button.spec.ts index ef4f23742db..eac0a0732d6 100644 --- a/core/src/components/button/test/button.spec.ts +++ b/core/src/components/button/test/button.spec.ts @@ -1,59 +1,57 @@ import { newSpecPage } from '@stencil/core/testing'; -import { initialize } from '../../../global/ionic-global'; +import { config } from '../../../global/config'; import { Button } from '../button'; describe('button: shape', () => { - beforeEach(() => { - // Registers the global script that reads the `theme` attribute - initialize(); + afterEach(() => { + config.reset({}); }); - it('should default to soft in the ios theme', async () => { + it('should default to round when the theme config has no shape', async () => { const page = await newSpecPage({ components: [Button], - html: `Button`, + html: `Button`, }); - expect(page.root!.classList.contains('button-soft')).toBe(true); + expect(page.root!.classList.contains('button-shape-round')).toBe(true); }); - it('should default to round in the md theme', async () => { - const page = await newSpecPage({ - components: [Button], - html: `Button`, - }); + it('should use the shape from the theme config', async () => { + config.reset({ customTheme: { config: { components: { IonButton: { shape: 'soft' } } } } }); - expect(page.root!.classList.contains('button-round')).toBe(true); - }); - - it('should default to round in the ionic theme', async () => { const page = await newSpecPage({ components: [Button], - html: `Button`, + html: `Button`, }); - expect(page.root!.classList.contains('button-round')).toBe(true); + expect(page.root!.classList.contains('button-shape-soft')).toBe(true); }); it('should use the shape when it is set', async () => { + config.reset({ customTheme: { config: { components: { IonButton: { shape: 'soft' } } } } }); + const page = await newSpecPage({ components: [Button], html: `Button`, }); - expect(page.root!.classList.contains('button-rectangular')).toBe(true); + expect(page.root!.classList.contains('button-shape-rectangular')).toBe(true); }); }); describe('button: fill', () => { + afterEach(() => { + config.reset({}); + }); + it('should default to solid', async () => { const page = await newSpecPage({ components: [Button], html: `Button`, }); - expect(page.root!.classList.contains('button-solid')).toBe(true); + expect(page.root!.classList.contains('button-fill-solid')).toBe(true); }); ['ion-buttons', 'ion-datetime', 'ion-list-header'].forEach((parent) => { @@ -64,8 +62,31 @@ describe('button: fill', () => { }); const button = page.body.querySelector('ion-button')!; - expect(button.classList.contains('button-clear')).toBe(true); + expect(button.classList.contains('button-fill-clear')).toBe(true); + }); + }); + + it('should use the fill from the theme config', async () => { + config.reset({ customTheme: { config: { components: { IonButton: { fill: 'outline' } } } } }); + + const page = await newSpecPage({ + components: [Button], + html: `Button`, }); + + expect(page.root!.classList.contains('button-fill-outline')).toBe(true); + }); + + it('should default to clear inside ion-buttons when the theme config has a fill', async () => { + config.reset({ customTheme: { config: { components: { IonButton: { fill: 'outline' } } } } }); + + const page = await newSpecPage({ + components: [Button], + html: `Button`, + }); + + const button = page.body.querySelector('ion-button')!; + expect(button.classList.contains('button-fill-clear')).toBe(true); }); it('should use the fill when it is set inside ion-buttons', async () => { @@ -75,18 +96,45 @@ describe('button: fill', () => { }); const button = page.body.querySelector('ion-button')!; - expect(button.classList.contains('button-solid')).toBe(true); + expect(button.classList.contains('button-fill-solid')).toBe(true); }); }); describe('button: size', () => { + afterEach(() => { + config.reset({}); + }); + it('should default to medium', async () => { const page = await newSpecPage({ components: [Button], html: `Button`, }); - expect(page.root!.classList.contains('button-medium')).toBe(true); + expect(page.root!.classList.contains('button-size-medium')).toBe(true); + }); + + it('should use the size from the theme config', async () => { + config.reset({ customTheme: { config: { components: { IonButton: { size: 'large' } } } } }); + + const page = await newSpecPage({ + components: [Button], + html: `Button`, + }); + + expect(page.root!.classList.contains('button-size-large')).toBe(true); + }); + + it('should default to small inside ion-item when the theme config has a size', async () => { + config.reset({ customTheme: { config: { components: { IonButton: { size: 'large' } } } } }); + + const page = await newSpecPage({ + components: [Button], + html: `Button`, + }); + + const button = page.body.querySelector('ion-button')!; + expect(button.classList.contains('button-size-small')).toBe(true); }); ['ion-item', 'ion-item-divider'].forEach((parent) => { @@ -97,8 +145,17 @@ describe('button: size', () => { }); const button = page.body.querySelector('ion-button')!; - expect(button.classList.contains('button-small')).toBe(true); + expect(button.classList.contains('button-size-small')).toBe(true); + }); + }); + + it('should render the deprecated default size as medium', async () => { + const page = await newSpecPage({ + components: [Button], + html: `Button`, }); + + expect(page.root!.classList.contains('button-size-medium')).toBe(true); }); it('should use the size when it is set inside ion-item', async () => { @@ -108,6 +165,32 @@ describe('button: size', () => { }); const button = page.body.querySelector('ion-button')!; - expect(button.classList.contains('button-large')).toBe(true); + expect(button.classList.contains('button-size-large')).toBe(true); + }); +}); + +describe('button: hue', () => { + afterEach(() => { + config.reset({}); + }); + + it('should default to bold', async () => { + const page = await newSpecPage({ + components: [Button], + html: `Button`, + }); + + expect(page.root!.classList.contains('button-hue-bold')).toBe(true); + }); + + it('should use the hue from the theme config', async () => { + config.reset({ customTheme: { config: { components: { IonButton: { hue: 'subtle' } } } } }); + + const page = await newSpecPage({ + components: [Button], + html: `Button`, + }); + + expect(page.root!.classList.contains('button-hue-subtle')).toBe(true); }); }); diff --git a/core/src/components/button/test/shape/button.e2e.ts b/core/src/components/button/test/shape/button.e2e.ts index 3724fd06161..f1e04988636 100644 --- a/core/src/components/button/test/shape/button.e2e.ts +++ b/core/src/components/button/test/shape/button.e2e.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; -const SHAPES = ['soft', 'round', 'rectangular']; +import { ION_BUTTON_SHAPES } from '../../button.interface'; // `solid` is the default fill, so it renders without a `fill` attribute const FILLS = ['solid', 'outline', 'clear']; @@ -14,7 +14,7 @@ const SIZES = ['small', 'medium', 'large']; configs({ modes: ['ionic-md', 'md', 'ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { test.describe(title('button: shape'), () => { test('should not have visual regressions', async ({ page }) => { - const grids = SHAPES.flatMap((shape) => + const grids = ION_BUTTON_SHAPES.flatMap((shape) => FILLS.map((fill) => { const fillAttr = fill === 'solid' ? '' : ` fill="${fill}"`; // Clear buttons have no background, so focus them to show the shape diff --git a/core/src/components/button/test/shape/index.html b/core/src/components/button/test/shape/index.html index e6f32cd5422..dd6b1fdaf40 100644 --- a/core/src/components/button/test/shape/index.html +++ b/core/src/components/button/test/shape/index.html @@ -89,6 +89,60 @@

Default

+
+

Crisp

+
+ + Button + Button + Button + + + + + + + + + + +
+ +
+ + Button + Button + Button + + + + + + + + + + +
+ +
+ + Button + Button + Button + + + + + + + + + + +
+
+

Soft

diff --git a/core/src/components/button/test/theme-ionic/index.html b/core/src/components/button/test/theme-ionic/index.html deleted file mode 100644 index 2449b482053..00000000000 --- a/core/src/components/button/test/theme-ionic/index.html +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - Button - Theme Ionic - - - - - - - - - - - - - - - Button - Ionic - - - - -

Preview options

- -

- - Small - Medium - Large - - - - default - Rectangular - Round - - - - default - Outline - Clear - - - - default - Primary - Success - Warning - Medium - Danger - -

- -
-

Preview

- -

- Button - - - -

- -

- - Button - - -

- -

- - Button - - -

- -

- - Button - - - -

- -

- - Button - - - - - -

-

- - Button - - -

- -

States

- -

- Default - - Button - - - - - -

- -

- Activated - - Button - - - - - -

- -

- Focused - - Button - - - - - -

- -

- Disabled - - Button - - - - - -

-
-
-
- - - - diff --git a/core/src/components/buttons/buttons.ionic.scss b/core/src/components/buttons/buttons.ionic.scss index 31f7e76cc4f..5cfff73f06d 100644 --- a/core/src/components/buttons/buttons.ionic.scss +++ b/core/src/components/buttons/buttons.ionic.scss @@ -9,16 +9,7 @@ --padding-bottom: 0; } -// Toolbar Clear Button -// -------------------------------------------------- - -::slotted(*) .button-clear { - --color: #{globals.$ion-text-default}; - --background: transparent; - --background-activated: transparent; - --background-focused: transparent; - --background-hover: transparent; -} +// Clear button colors are in the `IonToolbar.IonButtons.IonButton` tokens // Toolbar Button Icon // -------------------------------------------------- diff --git a/core/src/components/buttons/buttons.ios.scss b/core/src/components/buttons/buttons.ios.scss index abfbb42cc42..1371db2a668 100644 --- a/core/src/components/buttons/buttons.ios.scss +++ b/core/src/components/buttons/buttons.ios.scss @@ -21,62 +21,7 @@ --padding-bottom: 0; } -// TODO(FW-6203): is this value correct? -::slotted(*) ion-button:not(.button-round) { - --border-radius: #{$toolbar-ios-button-border-radius}; -} - -// iOS Toolbar with Color: Default Buttons -// -------------------------------------------------- - -:host-context(.ion-color)::slotted(*) .button { - --color: initial; - --border-color: initial; - --background-focused: #{current-color(contrast)}; -} - -:host-context(.ion-color)::slotted(*) .button-solid { - --background: #{current-color(contrast)}; - --background-focused: #000; - --background-focused-opacity: 0.12; - --background-activated: #000; - --background-activated-opacity: 0.12; - --background-hover: #{current-color(base)}; - --background-hover-opacity: 0.45; - --color: #{current-color(base)}; - --color-focused: #{current-color(base)}; -} - -:host-context(.ion-color)::slotted(*) .button-clear { - --color-activated: #{current-color(contrast)}; - --color-focused: #{current-color(contrast)}; -} - -:host-context(.ion-color)::slotted(*) .button-outline { - --color-activated: #{current-color(base)}; - --color-focused: #{current-color(contrast)}; - --background-activated: #{current-color(contrast)}; -} - -// iOS Toolbar Button Clear / Outline -// -------------------------------------------------- - -::slotted(*) .button-clear, -::slotted(*) .button-outline { - --background-activated: transparent; - --background-focused: currentColor; - --background-hover: transparent; -} - -// iOS Toolbar Button Solid -// -------------------------------------------------- - -::slotted(*) .button-solid:not(.ion-color) { - --background-focused: #000; - --background-focused-opacity: 0.12; - --background-activated: #000; - --background-activated-opacity: 0.12; -} +// Colors, shapes and fonts for the buttons are in the `IonToolbar.IonButtons.IonButton` tokens // iOS Toolbar Button Icon // -------------------------------------------------- diff --git a/core/src/components/buttons/buttons.md.scss b/core/src/components/buttons/buttons.md.scss index 64658af1fa4..7a4460f6930 100644 --- a/core/src/components/buttons/buttons.md.scss +++ b/core/src/components/buttons/buttons.md.scss @@ -10,7 +10,6 @@ --padding-bottom: 3px; --padding-start: 8px; --padding-end: 8px; - --box-shadow: none; @include margin-horizontal(2px, 2px); @@ -22,47 +21,17 @@ --padding-bottom: 0; } -// TODO(FW-6203): is this value correct? -::slotted(*) ion-button:not(.button-round) { - --border-radius: #{$toolbar-md-button-border-radius}; -} - -// Material Design Toolbar w/ Color: Default Buttons -// -------------------------------------------------- - -:host-context(.ion-color)::slotted(*) .button { - --color: initial; - --color-focused: #{current-color(contrast)}; - --color-hover: #{current-color(contrast)}; - --background-activated: transparent; - --background-focused: #{current-color(contrast)}; - --background-hover: #{current-color(contrast)}; -} - -:host-context(.ion-color)::slotted(*) .button-solid { - --background: #{current-color(contrast)}; - --background-activated: transparent; - --background-focused: #{current-color(shade)}; - --background-hover: #{current-color(base)}; - --color: #{current-color(base)}; - --color-focused: #{current-color(base)}; - --color-hover: #{current-color(base)}; -} - -:host-context(.ion-color)::slotted(*) .button-outline { - --border-color: #{current-color(contrast)}; -} +// Colors, shapes and fonts for the buttons are in the `IonToolbar.IonButtons.IonButton` tokens // Material Design Toolbar Icon Only Clear Button // -------------------------------------------------- // TODO(FW-6203): do we need these styles? -::slotted(*) .button-has-icon-only.button-clear { +::slotted(*) .button-has-icon-only.button-fill-clear { --padding-top: 12px; --padding-end: 12px; --padding-bottom: 12px; --padding-start: 12px; - --border-radius: 50%; @include margin(0); @@ -70,43 +39,6 @@ height: 3rem; } -::slotted(*) .button { - --background-hover: currentColor; -} - -// Material Design Toolbar Solid Button -// -------------------------------------------------- - -::slotted(*) .button-solid { - --color: #{$toolbar-md-background}; - --background: #{$toolbar-md-color}; - --background-activated: transparent; - --background-focused: currentColor; -} - -// Material Design Toolbar Outline Button -// -------------------------------------------------- - -::slotted(*) .button-outline { - --color: initial; - --background: transparent; - --background-activated: transparent; - --background-focused: currentColor; - --background-hover: currentColor; - --border-color: currentColor; -} - -// Material Design Toolbar Clear Button -// -------------------------------------------------- - -::slotted(*) .button-clear { - --color: initial; - --background: transparent; - --background-activated: transparent; - --background-focused: currentColor; - --background-hover: currentColor; -} - // Material Design Toolbar Button Icon // -------------------------------------------------- ::slotted(*) ion-icon[slot="start"] { diff --git a/core/src/components/datetime/datetime.ionic.scss b/core/src/components/datetime/datetime.ionic.scss index 42490906491..3fbd6ea4723 100644 --- a/core/src/components/datetime/datetime.ionic.scss +++ b/core/src/components/datetime/datetime.ionic.scss @@ -122,12 +122,20 @@ background: globals.$ion-primitives-neutral-200; } +/** + * TODO(FW-6893): Move to datetime's tokens in its migration. Button's migration only + * renamed these properties: a nested `IonDatetime.IonButton` token can't target just + * the previous and next buttons inside datetime's shadow root. + */ // TODO(ROU-11119): Rewrite this when we replace the ion-button with a native button .calendar-next-prev ion-button.button-has-icon-only { - --color: #{globals.$ion-primitives-neutral-1000}; - --background-activated: #{globals.$ion-primitives-neutral-200}; - --background-hover: #{globals.$ion-primitives-neutral-200}; - --background-hover-opacity: 1; + --ion-button-hue-bold-clear-default-color: #{globals.$ion-primitives-neutral-1000}; + --ion-button-hue-bold-clear-focus-color: #{globals.$ion-primitives-neutral-1000}; + --ion-button-hue-bold-clear-hover-color: #{globals.$ion-primitives-neutral-1000}; + --ion-button-hue-bold-clear-activated-color: #{globals.$ion-primitives-neutral-1000}; + --ion-button-hue-bold-clear-hover-background: #{globals.$ion-primitives-neutral-200}; + --ion-button-hue-bold-clear-activated-background: #{globals.$ion-primitives-neutral-200}; + --ion-button-hue-bold-clear-ripple-effect-color: #{globals.$ion-primitives-neutral-200}; min-height: globals.$ion-scale-1000; } diff --git a/core/src/components/datetime/datetime.md.scss b/core/src/components/datetime/datetime.md.scss index f5b09ccf0d0..6607ac2c5f0 100644 --- a/core/src/components/datetime/datetime.md.scss +++ b/core/src/components/datetime/datetime.md.scss @@ -47,8 +47,16 @@ // Calendar / Header / Action Buttons // ----------------------------------- +/** + * TODO(FW-6893): Move to datetime's tokens in its migration. Button's migration only + * renamed these properties: a nested `IonDatetime.IonButton` token can't target just + * the month navigation buttons inside datetime's shadow root. + */ :host .calendar-action-buttons ion-button { - --color: #{$text-color-step-350}; + --ion-button-hue-bold-clear-default-color: #{$text-color-step-350}; + --ion-button-hue-bold-clear-focus-color: #{$text-color-step-350}; + --ion-button-hue-bold-clear-hover-color: #{$text-color-step-350}; + --ion-button-hue-bold-clear-activated-color: #{$text-color-step-350}; } // These styles are copied from buttons.md.scss @@ -57,7 +65,6 @@ --padding-end: 12px; --padding-bottom: 12px; --padding-start: 12px; - --border-radius: 50%; @include margin(0); diff --git a/core/src/components/datetime/datetime.native.scss b/core/src/components/datetime/datetime.native.scss index 25cc2877f82..8479563af93 100644 --- a/core/src/components/datetime/datetime.native.scss +++ b/core/src/components/datetime/datetime.native.scss @@ -48,10 +48,6 @@ max-width: max-content; } -:host .calendar-action-buttons ion-button { - --background: transparent; -} - /** * Center the day text vertically * and horizontally within its grid cell. @@ -128,10 +124,18 @@ color: globals.current-color(base); } +/** + * TODO(FW-6893): Move to datetime's tokens in its migration. Button's migration only + * renamed these properties: a nested `IonDatetime.IonButton` token can't target just + * the action buttons inside datetime's shadow root. + */ // Default text color for action buttons (cancel, clear, done). // When datetime doesn't receive a color prop. :host .datetime-action-buttons ion-button { - --color: #{globals.ion-color(primary, base)}; + --ion-button-hue-bold-clear-default-color: #{globals.ion-color(primary, base)}; + --ion-button-hue-bold-clear-focus-color: #{globals.ion-color(primary, base)}; + --ion-button-hue-bold-clear-hover-color: #{globals.ion-color(primary, base)}; + --ion-button-hue-bold-clear-activated-color: #{globals.ion-color(primary, base)}; } .calendar-month-year-toggle { diff --git a/core/src/components/input-password-toggle/input-password-toggle.ionic.scss b/core/src/components/input-password-toggle/input-password-toggle.ionic.scss index a55370b5f09..b0a3019a213 100644 --- a/core/src/components/input-password-toggle/input-password-toggle.ionic.scss +++ b/core/src/components/input-password-toggle/input-password-toggle.ionic.scss @@ -3,7 +3,6 @@ // Ionic Input Toggle Password // -------------------------------------------------- :host ion-button { - --color: #{globals.$ion-primitives-neutral-800}; --margin-end: calc(-1 * var(--padding-end)); --padding-end: inherit; --icon-size: #{globals.$ion-font-size-400}; diff --git a/core/src/components/input/input.ios.scss b/core/src/components/input/input.ios.scss index 84747bd236c..0925a33120d 100644 --- a/core/src/components/input/input.ios.scss +++ b/core/src/components/input/input.ios.scss @@ -35,7 +35,6 @@ */ ::slotted(ion-button[slot="start"].button-has-icon-only), ::slotted(ion-button[slot="end"].button-has-icon-only) { - --border-radius: 50%; --padding-start: 0; --padding-end: 0; --padding-top: 0; diff --git a/core/src/components/input/input.md.scss b/core/src/components/input/input.md.scss index 2a796b35df2..fd2a4bf6c2b 100644 --- a/core/src/components/input/input.md.scss +++ b/core/src/components/input/input.md.scss @@ -115,7 +115,6 @@ */ ::slotted(ion-button[slot="start"].button-has-icon-only), ::slotted(ion-button[slot="end"].button-has-icon-only) { - --border-radius: 50%; --padding-start: 8px; --padding-end: 8px; --padding-top: 8px; diff --git a/core/src/components/item/item.common.scss b/core/src/components/item/item.common.scss index 044bf396531..75ce5e4460a 100644 --- a/core/src/components/item/item.common.scss +++ b/core/src/components/item/item.common.scss @@ -289,6 +289,10 @@ a { font-size: 1.6em; } +/** + * TODO(FW-6847): remove the `--margin-*` overrides, since + * `ion-button` no longer reads them. + */ ::slotted(ion-button) { --margin-top: 0; --margin-bottom: 0; diff --git a/core/src/components/item/item.ios.scss b/core/src/components/item/item.ios.scss index ac5e631a8c9..8071122da57 100644 --- a/core/src/components/item/item.ios.scss +++ b/core/src/components/item/item.ios.scss @@ -138,7 +138,7 @@ // iOS Item Button // -------------------------------------------------- -::slotted(.button-small) { +::slotted(.button-size-small) { --padding-top: 1px; --padding-bottom: 1px; --padding-start: 0.5em; diff --git a/core/src/components/item/item.md.scss b/core/src/components/item/item.md.scss index 15f7452170d..3114967ed38 100644 --- a/core/src/components/item/item.md.scss +++ b/core/src/components/item/item.md.scss @@ -236,7 +236,7 @@ // Material Design Item Button // -------------------------------------------------- -::slotted(.button-small) { +::slotted(.button-size-small) { --padding-top: 2px; --padding-bottom: 2px; --padding-start: 0.6em; diff --git a/core/src/components/select/select.ionic.scss b/core/src/components/select/select.ionic.scss index ebb2344053a..ce77a6bf96c 100644 --- a/core/src/components/select/select.ionic.scss +++ b/core/src/components/select/select.ionic.scss @@ -393,7 +393,6 @@ ::slotted(ion-button[slot="start"].button-has-icon-only), ::slotted(ion-button[slot="end"].button-has-icon-only) { - --border-radius: 50%; --padding-start: #{globals.$ion-space-200}; --padding-end: #{globals.$ion-space-200}; --padding-top: #{globals.$ion-space-200}; diff --git a/core/src/components/select/select.ios.scss b/core/src/components/select/select.ios.scss index 22d3217e153..91b12b2beb8 100644 --- a/core/src/components/select/select.ios.scss +++ b/core/src/components/select/select.ios.scss @@ -42,7 +42,6 @@ */ ::slotted(ion-button[slot="start"].button-has-icon-only), ::slotted(ion-button[slot="end"].button-has-icon-only) { - --border-radius: 50%; --padding-start: 0; --padding-end: 0; --padding-top: 0; diff --git a/core/src/components/select/select.md.scss b/core/src/components/select/select.md.scss index ec02ff63d87..1b9d6bfe75b 100644 --- a/core/src/components/select/select.md.scss +++ b/core/src/components/select/select.md.scss @@ -141,7 +141,6 @@ */ ::slotted(ion-button[slot="start"].button-has-icon-only), ::slotted(ion-button[slot="end"].button-has-icon-only) { - --border-radius: 50%; --padding-start: 8px; --padding-end: 8px; --padding-top: 8px; diff --git a/core/src/components/textarea/textarea.ios.scss b/core/src/components/textarea/textarea.ios.scss index 78f5c0ca157..ff84943f18b 100644 --- a/core/src/components/textarea/textarea.ios.scss +++ b/core/src/components/textarea/textarea.ios.scss @@ -64,7 +64,6 @@ */ ::slotted(ion-button[slot="start"].button-has-icon-only), ::slotted(ion-button[slot="end"].button-has-icon-only) { - --border-radius: 50%; --padding-start: 0; --padding-end: 0; --padding-top: 0; diff --git a/core/src/components/textarea/textarea.md.scss b/core/src/components/textarea/textarea.md.scss index 11c02763dd4..3db578635fe 100644 --- a/core/src/components/textarea/textarea.md.scss +++ b/core/src/components/textarea/textarea.md.scss @@ -116,7 +116,6 @@ */ ::slotted(ion-button[slot="start"].button-has-icon-only), ::slotted(ion-button[slot="end"].button-has-icon-only) { - --border-radius: 50%; --padding-start: 8px; --padding-end: 8px; --padding-top: 8px; diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index a150554be8e..78626e01b29 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -27,6 +27,13 @@ export const defaultTheme: DefaultTheme = { size: 'small', }, + IonButton: { + fill: 'solid', + hue: 'bold', + shape: 'round', + size: 'medium', + }, + IonChip: { fill: 'solid', hue: 'subtle', @@ -294,6 +301,771 @@ export const defaultTheme: DefaultTheme = { }, }, + IonButton: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + transition: 'none', + + font: { + family: 'var(--ion-font-family)', + }, + + letter: { + spacing: '0.01em', + }, + + // Hues + hue: { + bold: { + solid: { + strong: { + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + default: { + background: ionColor('primary', 'base'), + color: ionColor('primary', 'contrast'), + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: ionColor('primary', 'base'), + color: ionColor('primary', 'contrast'), + }, + + hover: { + background: ionColor('primary', 'shade'), + color: ionColor('primary', 'contrast'), + }, + + activated: { + background: ionColor('primary', 'shade'), + color: ionColor('primary', 'contrast'), + }, + + // A white wash over the button + disabled: { + background: ionColor('primary', 'base'), + + opacity: '0.4', + }, + + rippleEffect: { + color: ionColor('primary', 'shade'), + opacity: '1', + }, + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + default: { + background: currentColor('base'), + color: currentColor('contrast'), + }, + + focus: { + background: currentColor('base'), + color: currentColor('contrast'), + }, + + hover: { + background: currentColor('shade'), + color: currentColor('contrast'), + }, + + activated: { + background: currentColor('shade'), + color: currentColor('contrast'), + }, + + disabled: { + background: currentColor('base'), + }, + + rippleEffect: { + color: currentColor('shade'), + opacity: '1', + }, + }, + }, + + outline: { + strong: { + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + default: { + background: baseColors.backgroundColor, + color: ionColor('primary', 'foreground'), + + border: { + color: ionColor('primary', 'base'), + style: 'solid', + width: 'var(--ion-border-width-xxxs)', + }, + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: baseColors.backgroundColor, + color: ionColor('primary', 'foreground'), + }, + + hover: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'foreground'), + }, + + activated: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'foreground'), + }, + + // A white wash over the button + disabled: { + background: baseColors.backgroundColor, + + opacity: '0.4', + }, + + rippleEffect: { + color: ionColor('primary', 'shade', { subtle: true }), + opacity: '1', + }, + + semantic: { + default: { + background: baseColors.backgroundColor, + color: currentColor('foreground'), + + border: { + color: currentColor('base'), + }, + }, + + focus: { + background: baseColors.backgroundColor, + color: currentColor('foreground'), + }, + + hover: { + background: currentColor('shade', { subtle: true }), + color: currentColor('foreground'), + }, + + activated: { + background: currentColor('shade', { subtle: true }), + color: currentColor('foreground'), + }, + + disabled: { + background: baseColors.backgroundColor, + }, + + rippleEffect: { + color: currentColor('shade', { subtle: true }), + opacity: '1', + }, + }, + }, + + clear: { + strong: { + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'foreground'), + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: 'transparent', + color: ionColor('primary', 'foreground'), + }, + + hover: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'foreground'), + }, + + activated: { + background: ionColor('primary', 'shade', { subtle: true }), + color: ionColor('primary', 'foreground'), + }, + + // A white wash over the button + disabled: { + background: 'transparent', + + opacity: '0.4', + }, + + rippleEffect: { + color: ionColor('primary', 'shade', { subtle: true }), + opacity: '1', + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('foreground'), + }, + + focus: { + background: 'transparent', + color: currentColor('foreground'), + }, + + hover: { + background: currentColor('shade', { subtle: true }), + color: currentColor('foreground'), + }, + + activated: { + background: currentColor('shade', { subtle: true }), + color: currentColor('foreground'), + }, + + disabled: { + background: 'transparent', + }, + + rippleEffect: { + color: currentColor('shade', { subtle: true }), + opacity: '1', + }, + }, + }, + }, + }, + + // Sizes + size: { + small: { + minHeight: 'var(--ion-scaling-lg)', + minWidth: 'var(--ion-scaling-1600)', + + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-lg)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-lg)', + }, + + font: { + size: dynamicFont(global.root, 14), + }, + + line: { + height: dynamicFont(global.root, 20), + }, + + icon: { + font: { + size: dynamicFont(global.root, 20), + }, + + leading: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-sm)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + }, + + trailing: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-sm)', + }, + }, + }, + + iconOnly: { + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + icon: { + font: { + size: dynamicFont(global.root, 20), + }, + }, + }, + + spinner: { + // `xsmall` is the theme's default spinner size; each larger size steps up 4px + size: { + xsmall: { + height: 'var(--ion-scaling-xxs)', + width: 'var(--ion-scaling-xxs)', + }, + + small: { + height: 'var(--ion-scaling-xs)', + width: 'var(--ion-scaling-xs)', + }, + + medium: { + height: 'var(--ion-scaling-sm)', + width: 'var(--ion-scaling-sm)', + }, + + large: { + height: 'var(--ion-scaling-md)', + width: 'var(--ion-scaling-md)', + }, + + xlarge: { + height: 'var(--ion-scaling-900)', + width: 'var(--ion-scaling-900)', + }, + }, + + leading: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-sm)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + }, + + trailing: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-sm)', + }, + }, + }, + }, + + medium: { + minHeight: 'var(--ion-scaling-xl)', + minWidth: 'var(--ion-scaling-xxxl)', + + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-xl)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-xl)', + }, + + font: { + size: dynamicFont(global.root, 16), + }, + + line: { + height: dynamicFont(global.root, 24), + }, + + icon: { + font: { + size: dynamicFont(global.root, 20), + }, + + leading: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-250)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + }, + + trailing: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-250)', + }, + }, + }, + + iconOnly: { + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + icon: { + font: { + size: dynamicFont(global.root, 20), + }, + }, + }, + + spinner: { + // `xsmall` is the theme's default spinner size; each larger size steps up 4px + size: { + xsmall: { + height: 'var(--ion-scaling-xxs)', + width: 'var(--ion-scaling-xxs)', + }, + + small: { + height: 'var(--ion-scaling-xs)', + width: 'var(--ion-scaling-xs)', + }, + + medium: { + height: 'var(--ion-scaling-sm)', + width: 'var(--ion-scaling-sm)', + }, + + large: { + height: 'var(--ion-scaling-md)', + width: 'var(--ion-scaling-md)', + }, + + xlarge: { + height: 'var(--ion-scaling-900)', + width: 'var(--ion-scaling-900)', + }, + }, + + leading: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-250)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + }, + + trailing: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-250)', + }, + }, + }, + }, + + large: { + minHeight: 'var(--ion-scaling-xxl)', + minWidth: 'var(--ion-scaling-xxxxl)', + + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-xxxl)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-xxxl)', + }, + + font: { + size: dynamicFont(global.root, 20), + }, + + line: { + height: dynamicFont(global.root, 24), + }, + + icon: { + font: { + size: dynamicFont(global.root, 24), + }, + + leading: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-md)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + }, + + trailing: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-md)', + }, + }, + }, + + iconOnly: { + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + icon: { + font: { + size: dynamicFont(global.root, 24), + }, + }, + }, + + spinner: { + // `xsmall` is the theme's default spinner size; each larger size steps up 4px + size: { + xsmall: { + height: 'var(--ion-scaling-xs)', + width: 'var(--ion-scaling-xs)', + }, + + small: { + height: 'var(--ion-scaling-sm)', + width: 'var(--ion-scaling-sm)', + }, + + medium: { + height: 'var(--ion-scaling-md)', + width: 'var(--ion-scaling-md)', + }, + + large: { + height: 'var(--ion-scaling-900)', + width: 'var(--ion-scaling-900)', + }, + + xlarge: { + height: 'var(--ion-scaling-lg)', + width: 'var(--ion-scaling-lg)', + }, + }, + + leading: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-md)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + }, + + trailing: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-md)', + }, + }, + }, + }, + }, + + // Shapes + shape: { + crisp: { + border: { + radius: 'var(--ion-radii-sm)', + }, + }, + + soft: { + border: { + radius: 'var(--ion-radii-md)', + }, + }, + + round: { + border: { + radius: 'var(--ion-radii-xxxxl)', + }, + }, + + rectangular: { + border: { + radius: 'var(--ion-radii-xxxxs)', + }, + }, + }, + + // States + state: { + focus: { + ring: { + color: '#b5c0f7', + offset: 'var(--ion-border-width-xxs)', + style: 'solid', + width: 'var(--ion-border-width-xxs)', + }, + }, + }, + }, + + // Parents that theme the buttons they contain + IonInputPasswordToggle: { + IonButton: { + hue: { + bold: { + clear: { + default: { + color: '#6c727a', + }, + + focus: { + color: '#6c727a', + }, + + hover: { + color: '#6c727a', + }, + + activated: { + color: '#6c727a', + }, + }, + }, + }, + }, + }, + + IonToolbar: { + IonButton: { + /** + * In a toolbar without a color, the global `--ion-toolbar-color` and + * `--ion-toolbar-background` properties color the buttons. + */ + hue: { + bold: { + solid: { + default: { + background: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + + focus: { + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + + hover: { + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + + activated: { + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + }, + + // Transparent so the text, which takes the toolbar color, stays readable on any toolbar background + outline: { + default: { + background: 'transparent', + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + + border: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + }, + }, + + focus: { + background: 'transparent', + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + hover: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + disabled: { + background: 'transparent', + }, + }, + + clear: { + default: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + focus: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + hover: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + activated: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + }, + }, + }, + }, + + IonButtons: { + IonButton: { + hue: { + bold: { + // Clear buttons in a toolbar use the default text color and have no state backgrounds + clear: { + default: { + color: 'var(--ion-toolbar-color, #242528)', + }, + + focus: { + background: 'transparent', + color: 'var(--ion-toolbar-color, #242528)', + }, + + hover: { + background: 'transparent', + color: 'var(--ion-toolbar-color, #242528)', + }, + + activated: { + background: 'transparent', + color: 'var(--ion-toolbar-color, #242528)', + }, + + rippleEffect: { + color: 'transparent', + }, + }, + }, + }, + + // Toolbar buttons are shorter than standalone ones; icon-only ones are squares of this height + size: { + medium: { + minHeight: 'var(--ion-scaling-lg)', + }, + }, + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-0)', diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index 6a0a32a35dc..8ed064ee432 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -1,4 +1,4 @@ -import { rgba, currentColor, clamp, mix, dynamicFont, dynamicFontMin, ionColor } from '../../utils/theme'; +import { rgba, currentColor, clamp, mix, dynamicFont, dynamicFontMin, ionColor, overlay } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import { colors as baseColors } from '../base/shared.tokens'; import type { DefaultTheme } from '../themes.interfaces'; @@ -29,6 +29,13 @@ export const defaultTheme: DefaultTheme = { size: 'small', }, + IonButton: { + fill: 'solid', + hue: 'bold', + shape: 'soft', + size: 'medium', + }, + IonChip: { fill: 'solid', hue: 'bold', @@ -296,6 +303,756 @@ export const defaultTheme: DefaultTheme = { }, }, + IonButton: { + margin: { + top: 'var(--ion-spacing-xxs)', + end: 'var(--ion-spacing-xxxs)', + bottom: 'var(--ion-spacing-xxs)', + start: 'var(--ion-spacing-xxxs)', + }, + + transition: 'background-color, opacity 100ms linear', + + font: { + family: 'var(--ion-font-family, inherit)', + }, + + letter: { + spacing: '0', + }, + + // Hues + hue: { + bold: { + solid: { + strong: { + font: { + weight: 'var(--ion-font-weight-semi-bold)', + }, + }, + + default: { + background: ionColor('primary', 'base'), + color: ionColor('primary', 'contrast'), + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: ionColor('primary', 'shade'), + color: ionColor('primary', 'contrast'), + }, + + hover: { + background: ionColor('primary', 'tint'), + color: ionColor('primary', 'contrast'), + }, + + activated: { + background: ionColor('primary', 'shade'), + color: ionColor('primary', 'contrast'), + }, + + disabled: { + background: ionColor('primary', 'base'), + + opacity: '0.5', + }, + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + default: { + background: currentColor('base'), + color: currentColor('contrast'), + }, + + focus: { + background: currentColor('shade'), + color: currentColor('contrast'), + }, + + hover: { + background: currentColor('tint'), + color: currentColor('contrast'), + }, + + activated: { + background: currentColor('shade'), + color: currentColor('contrast'), + }, + + disabled: { + background: currentColor('base'), + }, + }, + }, + + outline: { + strong: { + font: { + weight: 'var(--ion-font-weight-semi-bold)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'base'), + + border: { + color: ionColor('primary', 'base'), + style: 'solid', + width: 'var(--ion-border-width-25)', + }, + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.1 }), + color: ionColor('primary', 'base'), + }, + + // Fades the whole button instead of changing its color + hover: { + background: 'transparent', + color: ionColor('primary', 'base'), + opacity: '0.6', + }, + + activated: { + background: ionColor('primary', 'base'), + color: ionColor('primary', 'contrast'), + }, + + disabled: { + background: 'transparent', + + opacity: '0.5', + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('base'), + + border: { + color: currentColor('base'), + }, + }, + + focus: { + background: currentColor('base', { alpha: 0.1 }), + color: currentColor('base'), + }, + + hover: { + background: 'transparent', + color: currentColor('base'), + }, + + activated: { + background: currentColor('base'), + color: currentColor('contrast'), + }, + + disabled: { + background: 'transparent', + }, + }, + }, + + clear: { + strong: { + font: { + weight: 'var(--ion-font-weight-semi-bold)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'foreground'), + + // The clear fill uses a regular weight font + font: { + weight: 'normal', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.1 }), + color: ionColor('primary', 'foreground'), + }, + + // Fades the whole button instead of changing its color + hover: { + background: 'transparent', + color: ionColor('primary', 'foreground'), + opacity: '0.6', + }, + + activated: { + background: 'transparent', + color: ionColor('primary', 'foreground'), + opacity: '0.4', + }, + + disabled: { + background: 'transparent', + + opacity: '0.5', + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('foreground'), + }, + + focus: { + background: currentColor('base', { alpha: 0.1 }), + color: currentColor('base'), + }, + + hover: { + background: 'transparent', + color: currentColor('base'), + }, + + activated: { + background: 'transparent', + color: currentColor('foreground'), + }, + + disabled: { + background: 'transparent', + }, + }, + }, + }, + }, + + // Sizes + size: { + small: { + minHeight: 'clamp(23px, 2.16em, 54px)', + + padding: { + top: 'var(--ion-spacing-xxs)', + end: '0.77em', + bottom: 'var(--ion-spacing-xxs)', + start: '0.77em', + }, + + font: { + size: `min(${dynamicFont(global.root, 13)}, 39px)`, + }, + + icon: { + font: { + size: '1.35em', + }, + + leading: { + margin: { + top: '0', + end: '0.3em', + bottom: '0', + start: '-0.3em', + }, + }, + + trailing: { + margin: { + top: '0', + end: '-0.2em', + bottom: '0', + start: '0.3em', + }, + }, + }, + + iconOnly: { + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + icon: { + font: { + /** + * The values were provided through a native iOS app. + * min font size: 12px, default font size: 17px, max font size: 40px + */ + size: 'clamp(12.1394px, 1.308125em, 40.1856px)', + }, + }, + }, + }, + + medium: { + minHeight: 'clamp(30px, 2.125em, 60px)', + + padding: { + top: '7px', + end: '0.875em', + bottom: '7px', + start: '0.875em', + }, + + font: { + size: `min(${dynamicFont(global.root, 16)}, 48px)`, + }, + + icon: { + font: { + size: '1.35em', + }, + + leading: { + margin: { + top: '0', + end: '0.3em', + bottom: '0', + start: '-0.3em', + }, + }, + + trailing: { + margin: { + top: '0', + end: '-0.2em', + bottom: '0', + start: '0.3em', + }, + }, + }, + + iconOnly: { + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + icon: { + font: { + /** + * The values were provided through a native iOS app. + * min font size: 15px, default font size: 18px, max font size: 43px + */ + size: 'clamp(15.12px, 1.125em, 43.02px)', + }, + }, + }, + }, + + large: { + minHeight: 'clamp(46px, 2.5em, 78px)', + + padding: { + top: '14px', + end: '1em', + bottom: '14px', + start: '1em', + }, + + font: { + size: `min(${dynamicFont(global.root, 20)}, 60px)`, + }, + + icon: { + font: { + size: '1.35em', + }, + + leading: { + margin: { + top: '0', + end: '0.3em', + bottom: '0', + start: '-0.3em', + }, + }, + + trailing: { + margin: { + top: '0', + end: '-0.2em', + bottom: '0', + start: '0.3em', + }, + }, + }, + + iconOnly: { + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + icon: { + font: { + /** + * The values were provided through a native iOS app. + * min font size: 15px, default font size: 18px, max font size: 43px + */ + size: 'clamp(15.12px, 0.9em, 43.056px)', + }, + }, + }, + }, + }, + + // Shapes + shape: { + crisp: { + border: { + radius: 'var(--ion-radii-xxs)', + }, + }, + + soft: { + border: { + radius: 'var(--ion-radii-xs)', + }, + }, + + round: { + border: { + radius: 'var(--ion-radii-xxxxl)', + }, + }, + + rectangular: { + border: { + radius: 'var(--ion-radii-xxxxs)', + }, + }, + }, + }, + + // Parents that theme the buttons they contain + IonDatetime: { + IonButton: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-xxxs)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-xxxs)', + }, + + size: { + medium: { + minHeight: 'var(--ion-scaling-md)', + + padding: { + top: 'var(--ion-spacing-75)', + end: '5px', + bottom: 'var(--ion-spacing-75)', + start: '5px', + }, + + font: { + size: 'clamp(17px, 1.0625rem, 21.08px)', + }, + }, + }, + + hue: { + bold: { + solid: { + default: { + font: { + weight: 'var(--ion-font-weight-normal)', + }, + }, + }, + + outline: { + default: { + font: { + weight: 'var(--ion-font-weight-normal)', + }, + }, + }, + + clear: { + default: { + font: { + weight: 'var(--ion-font-weight-normal)', + }, + }, + }, + }, + }, + }, + }, + + IonToolbar: { + IonButton: { + /** + * In a toolbar without a color, the global `--ion-toolbar-color` and + * `--ion-toolbar-background` properties color the buttons. + */ + hue: { + bold: { + solid: { + default: { + background: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + + focus: { + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + + // A solid button lightens the toolbar color on hover + hover: { + background: overlay( + `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + 'rgba(255, 255, 255, 0.1)' + ), + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + + activated: { + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + }, + + outline: { + default: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + + border: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + }, + }, + + focus: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + }, + + hover: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + }, + + // A pressed outline button fills with the toolbar color + activated: { + background: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + }, + }, + + clear: { + default: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + focus: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + hover: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + activated: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + }, + }, + }, + }, + + IonButtons: { + IonButton: { + // Buttons in a toolbar use a larger font and a regular weight + size: { + medium: { + font: { + size: 'clamp(17px, 1.0625rem, 21.08px)', + }, + }, + }, + + shape: { + soft: { + border: { + radius: 'var(--ion-radii-xxs)', + }, + }, + + rectangular: { + border: { + radius: 'var(--ion-radii-xxs)', + }, + }, + }, + + hue: { + bold: { + solid: { + default: { + font: { + weight: 'var(--ion-font-weight-normal)', + }, + }, + + focus: { + background: overlay( + `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + 'rgba(0, 0, 0, 0.12)' + ), + }, + + activated: { + background: overlay( + `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + 'rgba(0, 0, 0, 0.12)' + ), + }, + }, + + // Outline and clear tints use primary, unless the app sets `--ion-toolbar-color` + outline: { + default: { + font: { + weight: 'var(--ion-font-weight-normal)', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.1 }), + }, + + hover: { + background: 'transparent', + }, + + activated: { + background: 'transparent', + }, + }, + + clear: { + default: { + font: { + weight: 'var(--ion-font-weight-normal)', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.1 }), + }, + + hover: { + background: 'transparent', + }, + + activated: { + background: 'transparent', + }, + }, + }, + }, + }, + }, + + // In a toolbar with a color + semantic: { + IonButtons: { + IonButton: { + hue: { + bold: { + solid: { + default: { + background: currentColor('contrast'), + color: currentColor('base'), + }, + + focus: { + background: overlay(currentColor('contrast'), 'rgba(0, 0, 0, 0.12)'), + color: currentColor('base'), + }, + + hover: { + background: overlay(currentColor('contrast'), currentColor('base', { alpha: 0.45 })), + color: currentColor('base'), + }, + + activated: { + background: overlay(currentColor('contrast'), 'rgba(0, 0, 0, 0.12)'), + color: currentColor('base'), + }, + }, + + outline: { + default: { + background: 'transparent', + color: 'currentColor', + + border: { + color: 'currentColor', + }, + }, + + focus: { + background: currentColor('contrast', { alpha: 0.1 }), + color: currentColor('contrast'), + }, + + hover: { + background: 'transparent', + color: 'currentColor', + }, + + activated: { + background: currentColor('contrast'), + color: currentColor('base'), + }, + }, + + clear: { + default: { + background: 'transparent', + color: 'currentColor', + + border: { + color: 'currentColor', + }, + }, + + focus: { + background: currentColor('contrast', { alpha: 0.1 }), + color: currentColor('contrast'), + }, + + hover: { + background: 'transparent', + color: 'currentColor', + }, + + activated: { + background: 'transparent', + color: currentColor('contrast'), + }, + }, + }, + }, + }, + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-xxs)', diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index 9e7626e67cb..2c3fff483cb 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -1,4 +1,4 @@ -import { rgba, currentColor, mix, dynamicFont, ionColor } from '../../utils/theme'; +import { rgba, currentColor, mix, dynamicFont, ionColor, overlay } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import { colors as baseColors } from '../base/shared.tokens'; import type { DefaultTheme } from '../themes.interfaces'; @@ -32,6 +32,13 @@ export const defaultTheme: DefaultTheme = { size: 'medium', }, + IonButton: { + fill: 'solid', + hue: 'bold', + shape: 'round', + size: 'medium', + }, + IonChip: { fill: 'solid', hue: 'bold', @@ -305,6 +312,763 @@ export const defaultTheme: DefaultTheme = { }, }, + IonButton: { + margin: { + top: 'var(--ion-spacing-xxs)', + end: 'var(--ion-spacing-xxxs)', + bottom: 'var(--ion-spacing-xxs)', + start: 'var(--ion-spacing-xxxs)', + }, + + transition: 'box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1), background-color 15ms linear, color 15ms linear', + + font: { + family: 'var(--ion-font-family, inherit)', + }, + + letter: { + spacing: '0.06em', + }, + + text: { + transform: 'uppercase', + }, + + // Hues + hue: { + bold: { + solid: { + strong: { + font: { + weight: 'var(--ion-font-weight-bold)', + }, + }, + + default: { + background: ionColor('primary', 'base'), + color: ionColor('primary', 'contrast'), + + box: { + shadow: + '0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12)', + }, + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: overlay(ionColor('primary', 'base'), ionColor('primary', 'contrast', { alpha: 0.24 })), + color: ionColor('primary', 'contrast'), + }, + + hover: { + background: overlay(ionColor('primary', 'base'), ionColor('primary', 'contrast', { alpha: 0.08 })), + color: ionColor('primary', 'contrast'), + }, + + // The ripple is the pressed feedback + activated: { + background: ionColor('primary', 'base'), + color: ionColor('primary', 'contrast'), + + box: { + shadow: + '0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12)', + }, + }, + + disabled: { + background: ionColor('primary', 'base'), + + opacity: '0.5', + }, + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + default: { + background: currentColor('base'), + color: currentColor('contrast'), + }, + + focus: { + background: overlay(currentColor('base'), currentColor('contrast', { alpha: 0.24 })), + color: currentColor('contrast'), + }, + + hover: { + background: overlay(currentColor('base'), currentColor('contrast', { alpha: 0.08 })), + color: currentColor('contrast'), + }, + + activated: { + background: currentColor('base'), + color: currentColor('contrast'), + }, + + disabled: { + background: currentColor('base'), + }, + }, + }, + + outline: { + strong: { + font: { + weight: 'var(--ion-font-weight-bold)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'base'), + + border: { + color: ionColor('primary', 'base'), + style: 'solid', + width: 'var(--ion-border-width-xxs)', + }, + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.12 }), + color: ionColor('primary', 'base'), + }, + + hover: { + background: ionColor('primary', 'base', { alpha: 0.04 }), + color: ionColor('primary', 'base'), + }, + + // The ripple is the pressed feedback + activated: { + background: 'transparent', + color: ionColor('primary', 'base'), + }, + + disabled: { + background: 'transparent', + + opacity: '0.5', + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('base'), + + border: { + color: currentColor('base'), + }, + }, + + focus: { + background: currentColor('base', { alpha: 0.12 }), + color: currentColor('base'), + }, + + hover: { + background: currentColor('base', { alpha: 0.04 }), + color: currentColor('base'), + }, + + activated: { + background: 'transparent', + color: currentColor('base'), + }, + + disabled: { + background: 'transparent', + }, + }, + }, + + clear: { + strong: { + font: { + weight: 'var(--ion-font-weight-bold)', + }, + }, + + default: { + background: 'transparent', + color: ionColor('primary', 'foreground'), + + font: { + weight: 'var(--ion-font-weight-medium)', + }, + }, + + focus: { + background: ionColor('primary', 'base', { alpha: 0.12 }), + color: ionColor('primary', 'foreground'), + }, + + hover: { + background: ionColor('primary', 'base', { alpha: 0.04 }), + color: ionColor('primary', 'foreground'), + }, + + // The ripple is the pressed feedback + activated: { + background: 'transparent', + color: ionColor('primary', 'foreground'), + }, + + disabled: { + background: 'transparent', + + opacity: '0.5', + }, + + semantic: { + default: { + background: 'transparent', + color: currentColor('foreground'), + }, + + focus: { + background: currentColor('base', { alpha: 0.12 }), + color: currentColor('foreground'), + }, + + hover: { + background: currentColor('base', { alpha: 0.04 }), + color: currentColor('foreground'), + }, + + activated: { + background: 'transparent', + color: currentColor('foreground'), + }, + + disabled: { + background: 'transparent', + }, + }, + }, + }, + }, + + // Sizes + size: { + small: { + minHeight: '2.1em', + + padding: { + top: 'var(--ion-spacing-xxs)', + end: '0.9em', + bottom: 'var(--ion-spacing-xxs)', + start: '0.9em', + }, + + font: { + size: dynamicFont(global.root, 13), + }, + + icon: { + font: { + size: '1.35em', + }, + + leading: { + margin: { + top: '0', + end: '0.3em', + bottom: '0', + start: '-0.3em', + }, + }, + + trailing: { + margin: { + top: '0', + end: '-0.2em', + bottom: '0', + start: '0.3em', + }, + }, + }, + + // MD does not specify a small size, so these are based on the iOS small size + iconOnly: { + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + icon: { + font: { + size: 'clamp(13.002px, 1.23125em, 40.385px)', + }, + }, + }, + }, + + medium: { + minHeight: 'var(--ion-scaling-900)', + + padding: { + top: 'var(--ion-spacing-sm)', + end: '1.1em', + bottom: 'var(--ion-spacing-sm)', + start: '1.1em', + }, + + font: { + size: dynamicFont(global.root, 14), + }, + + icon: { + font: { + size: '1.35em', + }, + + leading: { + margin: { + top: '0', + end: '0.3em', + bottom: '0', + start: '-0.3em', + }, + }, + + trailing: { + margin: { + top: '0', + end: '-0.2em', + bottom: '0', + start: '0.3em', + }, + }, + }, + + iconOnly: { + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + icon: { + font: { + /** + * The values were provided through MD design. + * min font size: 15px, default font size: 22.4px, max font size: 43px + */ + size: 'clamp(15.104px, 1.6em, 43.008px)', + }, + }, + }, + }, + + large: { + minHeight: '2.8em', + + padding: { + top: 'var(--ion-spacing-350)', + end: '1em', + bottom: 'var(--ion-spacing-350)', + start: '1em', + }, + + font: { + size: dynamicFont(global.root, 20), + }, + + icon: { + font: { + size: '1.35em', + }, + + leading: { + margin: { + top: '0', + end: '0.3em', + bottom: '0', + start: '-0.3em', + }, + }, + + trailing: { + margin: { + top: '0', + end: '-0.2em', + bottom: '0', + start: '0.3em', + }, + }, + }, + + // MD does not specify a large size, so these are based on the iOS large size + iconOnly: { + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + }, + + icon: { + font: { + size: 'clamp(15.008px, 1.4em, 43.008px)', + }, + }, + }, + }, + }, + + // Shapes + shape: { + crisp: { + border: { + radius: 'var(--ion-radii-xxs)', + }, + }, + + soft: { + border: { + radius: 'var(--ion-radii-sm)', + }, + }, + + round: { + border: { + radius: 'var(--ion-radii-xxxxl)', + }, + }, + + rectangular: { + border: { + radius: 'var(--ion-radii-xxxxs)', + }, + }, + }, + }, + + // Parents that theme the buttons they contain + IonDatetime: { + IonButton: { + margin: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-xxxs)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-xxxs)', + }, + + size: { + medium: { + minHeight: 'var(--ion-scaling-md)', + + padding: { + top: 'var(--ion-spacing-75)', + end: 'var(--ion-spacing-sm)', + bottom: 'var(--ion-spacing-75)', + start: 'var(--ion-spacing-sm)', + }, + }, + }, + + hue: { + bold: { + solid: { + default: { + box: { + shadow: 'none', + }, + }, + + activated: { + box: { + shadow: 'none', + }, + }, + }, + + // Datetime renders primary buttons, so their tints use primary + clear: { + focus: { + background: ionColor('primary', 'base', { alpha: 0.12 }), + }, + + hover: { + background: ionColor('primary', 'base', { alpha: 0.04 }), + }, + }, + }, + }, + }, + }, + + IonToolbar: { + IonButton: { + /** + * In a toolbar without a color, the global `--ion-toolbar-color` and + * `--ion-toolbar-background` properties color the buttons. + */ + hue: { + bold: { + solid: { + default: { + background: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + + focus: { + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + + hover: { + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + + activated: { + color: `var(--ion-toolbar-background, ${ionColor('primary', 'contrast')})`, + }, + }, + + outline: { + default: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + + border: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + }, + }, + + focus: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + }, + + hover: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'base')})`, + }, + }, + + clear: { + default: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + focus: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + hover: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + + activated: { + color: `var(--ion-toolbar-color, ${ionColor('primary', 'foreground')})`, + }, + }, + }, + }, + }, + + IonButtons: { + IonButton: { + shape: { + soft: { + border: { + radius: 'var(--ion-radii-xxs)', + }, + }, + + rectangular: { + border: { + radius: 'var(--ion-radii-xxs)', + }, + }, + }, + + hue: { + bold: { + // The solid fill inverts the toolbar colors + solid: { + default: { + background: 'var(--ion-toolbar-color, var(--ion-text-color, #424242))', + color: 'var(--ion-toolbar-background, var(--ion-background-color, #fff))', + + box: { + shadow: 'none', + }, + }, + + /** + * The tint is the button's text color, which matches the page background + * unless the app gives toolbars their own `--ion-toolbar-background`. + */ + focus: { + background: overlay( + 'var(--ion-toolbar-color, var(--ion-text-color, #424242))', + 'rgba(var(--ion-background-color-rgb, 255, 255, 255), 0.24)' + ), + color: 'var(--ion-toolbar-background, var(--ion-background-color, #fff))', + }, + + hover: { + background: overlay( + 'var(--ion-toolbar-color, var(--ion-text-color, #424242))', + 'rgba(var(--ion-background-color-rgb, 255, 255, 255), 0.08)' + ), + color: 'var(--ion-toolbar-background, var(--ion-background-color, #fff))', + }, + + activated: { + background: 'var(--ion-toolbar-color, var(--ion-text-color, #424242))', + color: 'var(--ion-toolbar-background, var(--ion-background-color, #fff))', + + box: { + shadow: 'none', + }, + }, + }, + + // Outline and clear tints use the toolbar's text color, #424242 unless set + outline: { + default: { + background: 'transparent', + color: 'var(--ion-toolbar-color, currentColor)', + + border: { + color: 'var(--ion-toolbar-color, currentColor)', + }, + }, + + focus: { + background: 'rgba(var(--ion-text-color-rgb, 66, 66, 66), 0.12)', + color: 'var(--ion-toolbar-color, currentColor)', + }, + + hover: { + background: 'rgba(var(--ion-text-color-rgb, 66, 66, 66), 0.04)', + color: 'var(--ion-toolbar-color, currentColor)', + }, + + activated: { + background: 'transparent', + color: 'currentColor', + }, + }, + + clear: { + default: { + background: 'transparent', + color: 'var(--ion-toolbar-color, currentColor)', + }, + + focus: { + background: 'rgba(var(--ion-text-color-rgb, 66, 66, 66), 0.12)', + color: 'var(--ion-toolbar-color, currentColor)', + }, + + hover: { + background: 'rgba(var(--ion-text-color-rgb, 66, 66, 66), 0.04)', + color: 'var(--ion-toolbar-color, currentColor)', + }, + + activated: { + background: 'transparent', + color: 'var(--ion-toolbar-color, currentColor)', + }, + }, + }, + }, + }, + }, + + // In a toolbar with a color + semantic: { + IonButtons: { + IonButton: { + hue: { + bold: { + solid: { + default: { + background: currentColor('contrast'), + color: currentColor('base'), + }, + + // `shade` has no `-rgb` variable, so `base` stands in for it + focus: { + background: overlay(currentColor('contrast'), currentColor('base', { alpha: 0.24 })), + color: currentColor('base'), + }, + + hover: { + background: overlay(currentColor('contrast'), currentColor('base', { alpha: 0.08 })), + color: currentColor('base'), + }, + + activated: { + background: currentColor('contrast'), + color: currentColor('base'), + }, + }, + + outline: { + default: { + background: 'transparent', + color: 'currentColor', + + border: { + color: currentColor('contrast'), + }, + }, + + focus: { + background: currentColor('contrast', { alpha: 0.12 }), + color: currentColor('contrast'), + }, + + hover: { + background: currentColor('contrast', { alpha: 0.04 }), + color: currentColor('contrast'), + }, + + activated: { + background: 'transparent', + color: 'currentColor', + }, + }, + + clear: { + default: { + background: 'transparent', + color: 'currentColor', + }, + + focus: { + background: currentColor('contrast', { alpha: 0.12 }), + color: currentColor('contrast'), + }, + + hover: { + background: currentColor('contrast', { alpha: 0.04 }), + color: currentColor('contrast'), + }, + + activated: { + background: 'transparent', + color: 'currentColor', + }, + }, + }, + }, + }, + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-xxs)', diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index 2dd54367014..d1a72958ef2 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -1,4 +1,5 @@ import type { IonBadgeConfig, IonBadgeRecipe } from '../components/badge/badge.interfaces'; +import type { IonButtonConfig, IonButtonRecipe } from '../components/button/button.interface'; import type { IonChipConfig, IonChipRecipe } from '../components/chip/chip.interfaces'; import type { IonColRecipe } from '../components/col/col.interface'; import type { IonContentRecipe } from '../components/content/content.interfaces'; @@ -253,6 +254,7 @@ export type BaseTheme = { export type IonicConfig = IonicGlobalConfig & { components?: { IonBadge?: IonBadgeConfig; + IonButton?: IonButtonConfig; IonChip?: IonChipConfig; IonProgressBar?: IonProgressBarConfig; IonSpinner?: IonSpinnerConfig; @@ -295,6 +297,7 @@ export type DefaultTheme = BaseTheme & { type Components = { IonAvatar?: any; IonBadge?: IonBadgeRecipe; + IonButton?: IonButtonRecipe; IonChip?: IonChipRecipe; IonCol?: IonColRecipe; IonContent?: IonContentRecipe; @@ -310,9 +313,38 @@ type Components = { IonCard?: any; IonItem?: any; + + /** + * Parents that style the buttons inside them. When a parent migrates, + * its recipe type replaces the entry here and keeps the `IonButton` key. + */ + IonDatetime?: { + IonButton?: IonButtonRecipe; + }; + + IonInputPasswordToggle?: { + IonButton?: IonButtonRecipe; + }; + + IonToolbar?: { + // TODO(FW-6864): Replace with the toolbar recipe once IonToolbar migrates + [key: string]: any; + IonButton?: IonButtonRecipe; + + IonButtons?: { + IonButton?: IonButtonRecipe; + }; + + // In a toolbar with a color + semantic?: { + IonButtons?: { + IonButton?: IonButtonRecipe; + }; + }; + }; + IonTabBar?: any; IonModal?: any; - IonToolbar?: any; }; export type IonMargin = { diff --git a/core/src/utils/test/theme.spec.ts b/core/src/utils/test/theme.spec.ts index 4302a20c351..ca197d0993c 100644 --- a/core/src/utils/test/theme.spec.ts +++ b/core/src/utils/test/theme.spec.ts @@ -1,19 +1,26 @@ import { newSpecPage } from '@stencil/core/testing'; +import { readdirSync, readFileSync } from 'fs'; +import { join } from 'path'; import { Buttons } from '../../components/buttons/buttons'; import { CardContent } from '../../components/card-content/card-content'; import { Chip } from '../../components/chip/chip'; +import { defaultTheme as ionicTheme } from '../../themes/ionic/default.tokens'; +import { defaultTheme as iosTheme } from '../../themes/ios/default.tokens'; +import { defaultTheme as mdTheme } from '../../themes/md/default.tokens'; import { generateColorClasses, generateComponentsThemeCSS, generateCSSVars, generateGlobalThemeCSS, getClassList, + INTERNAL_CHILD_COMPONENTS, getClassMap, getCustomTheme, hexToRgb, injectCSS, mix, + overlay, } from '../theme'; describe('getClassList()', () => { @@ -842,6 +849,244 @@ describe('generateComponentsThemeCSS', () => { expect(css).toBe(''); }); + + it('should generate a descendant block for a child component', () => { + const components = { + IonButtons: { + gap: '4px', + IonButton: { + padding: { top: '0px' }, + }, + }, + }; + + const css = generateComponentsThemeCSS(components).replace(/\s/g, ''); + + const expectedCSS = ` + ion-buttons { + --ion-buttons-gap: 4px; + } + + ion-buttons ion-button { + --ion-button-padding-top: 0px; + } + `.replace(/\s/g, ''); + + expect(css).toBe(expectedCSS); + }); + + it('should add the parents that render a child internally to its selectors', () => { + const components = { + IonButton: { + padding: { top: '13px' }, + }, + IonDatetime: { + IonButton: { + padding: { top: '3px' }, + }, + }, + }; + + const css = generateComponentsThemeCSS(components).replace(/\s/g, ''); + + const expectedCSS = ` + ion-button, + ion-datetime, + ion-input-password-toggle { + --ion-button-padding-top: 13px; + } + + ion-datetime, + ion-datetime ion-button { + --ion-button-padding-top: 3px; + } + `.replace(/\s/g, ''); + + expect(css).toBe(expectedCSS); + }); + + it('should generate child blocks after every component block', () => { + const components = { + IonButtons: { + IonButton: { + padding: { top: '0px' }, + }, + }, + IonChip: { + gap: '4px', + }, + }; + + const css = generateComponentsThemeCSS(components).replace(/\s/g, ''); + + const expectedCSS = ` + ion-chip { + --ion-chip-gap: 4px; + } + + ion-buttons ion-button { + --ion-button-padding-top: 0px; + } + `.replace(/\s/g, ''); + + expect(css).toBe(expectedCSS); + }); + + it('should not generate a block for a parent that only themes its children', () => { + const components = { + IonToolbar: { + IonButtons: { + IonButton: { + color: 'red', + }, + }, + }, + }; + + const css = generateComponentsThemeCSS(components); + + expect(css.replace(/\s/g, '')).toBe( + ` + ion-toolbar ion-buttons ion-button { + --ion-button-color: red; + } + `.replace(/\s/g, '') + ); + }); + + it('should add the color class to the parent of a semantic child', () => { + const components = { + IonToolbar: { + IonButtons: { + IonButton: { + color: 'red', + }, + }, + semantic: { + IonButtons: { + IonButton: { + color: 'blue', + }, + }, + }, + }, + }; + + const css = generateComponentsThemeCSS(components).replace(/\s/g, ''); + + const expectedCSS = ` + ion-toolbar ion-buttons ion-button { + --ion-button-color: red; + } + + ion-toolbar.ion-color ion-buttons ion-button { + --ion-button-color: blue; + } + `.replace(/\s/g, ''); + + expect(css).toBe(expectedCSS); + }); + + it('should keep the semantic tokens that are not a child as the parent tokens', () => { + const components = { + IonToolbar: { + semantic: { + background: 'red', + IonButton: { + color: 'blue', + }, + }, + }, + }; + + const css = generateComponentsThemeCSS(components).replace(/\s/g, ''); + + const expectedCSS = ` + ion-toolbar { + --ion-toolbar-semantic-background: red; + } + + ion-toolbar.ion-color ion-button { + --ion-button-color: blue; + } + `.replace(/\s/g, ''); + + expect(css).toBe(expectedCSS); + }); +}); + +describe('INTERNAL_CHILD_COMPONENTS', () => { + const COMPONENTS_DIR = join(__dirname, '../../components'); + + const toPascalCase = (tag: string) => + tag + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(''); + + const isChild = (key: string) => /^Ion[A-Z]/.test(key); + + /** + * Migrated components have tokens of their own in at least one theme. Entries + * that only nest children, directly or under `semantic`, are skipped. + */ + const migratedComponents = [ + ...new Set( + [iosTheme, mdTheme, ionicTheme].flatMap((theme) => + Object.entries(theme.components ?? {}) + .filter(([, tokens]) => + Object.entries(tokens ?? {}).some(([key, value]) => + key === 'semantic' ? Object.keys(value ?? {}).some((semanticKey) => !isChild(semanticKey)) : !isChild(key) + ) + ) + .map(([name]) => name) + ) + ), + ]; + + /** + * The ripple's only token falls back to the same value in its stylesheet, + * so it renders correctly inside a shadow root without being listed. + */ + const exempt = ['IonRippleEffect']; + + /** + * Token CSS can't reach a migrated child that a parent renders in its own shadow + * root, like datetime's buttons, unless the parent is in `INTERNAL_CHILD_COMPONENTS`. + * A missing entry fails silently, so this reads each shadow component's TSX. + */ + it('should list every migrated component rendered in a shadow root', () => { + const missing: string[] = []; + + for (const dir of readdirSync(COMPONENTS_DIR)) { + const file = join(COMPONENTS_DIR, dir, `${dir}.tsx`); + let source: string; + try { + source = readFileSync(file, 'utf8'); + } catch { + continue; + } + + if (!source.includes('shadow: true')) { + continue; + } + + const parent = toPascalCase(`ion-${dir}`); + const renderedTags = new Set([...source.matchAll(/<(ion-[a-z-]+)[\s>/]/g)].map((match) => match[1])); + + for (const tag of renderedTags) { + const child = toPascalCase(tag); + if (child === parent || exempt.includes(child) || !migratedComponents.includes(child)) { + continue; + } + if (!INTERNAL_CHILD_COMPONENTS[parent]?.includes(child)) { + missing.push(`${parent} renders ${child}`); + } + } + } + + expect(missing).toEqual([]); + }); }); describe('generateColorClasses', () => { @@ -1055,3 +1300,11 @@ describe('mix()', () => { expect(mix('#123456', '#abcdef', '100%')).toBe('#abcdef'); }); }); + +describe('overlay()', () => { + it('should lay the translucent color over the base color as one background value', () => { + expect(overlay('var(--ion-color-primary)', 'rgba(255, 255, 255, 0.24)')).toBe( + 'linear-gradient(rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0.24)) var(--ion-color-primary)' + ); + }); +}); diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index a9df8660f0b..88357fec878 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -464,24 +464,203 @@ export const applyGlobalTheme = (baseTheme: any, userTheme?: any): any => { * @returns string containing the component's themed CSS variables */ export const generateComponentsThemeCSS = (components: any): string => { - let css = ''; + const componentBlocks: string[] = []; + const childBlocks: string[] = []; for (const [component, componentTokens] of Object.entries(components)) { const componentTag = convertToKebabCase(component); - const vars = generateCSSVars(componentTokens, `--${componentTag}-`); + const { ownTokens, children } = splitChildComponents(componentTokens, componentTag); + + /** + * The component's own tokens, set on its tag and on any parent that renders + * it inside a shadow root (refer to `INTERNAL_CHILD_COMPONENTS`): + * `ion-button, ion-datetime, ion-input-password-toggle { --ion-button-*: … }`. + * + * An entry that only nests children has no tokens of its own, so it gets no + * empty block. + */ + if (Object.keys(ownTokens).length > 0 || children.length === 0) { + const selectors = [componentTag, ...getInternalRenderHosts(component)]; + componentBlocks.push(generateBlock(selectors, ownTokens, componentTag)); + } + + /** + * A nested child's tokens, scoped to the parent: + * `IonToolbar: { IonButtons: { IonButton } }` sets the button's own + * `--ion-button-*` properties in `ion-toolbar ion-buttons ion-button { … }`. + */ + for (const [child, childTokens, parentSelector] of children) { + childBlocks.push(...generateChildBlocks(component, child, childTokens, parentSelector)); + } + } + + /** + * Child blocks come last, so a parent's overrides always win over the child's own tokens. + * A shadow root host sets both at the same specificity, so only the order decides: + * `ion-button, ion-datetime { --ion-button-*: … }` then + * `ion-datetime, ion-datetime ion-button { --ion-button-*: … }`. + */ + return [...componentBlocks, ...childBlocks].join(''); +}; - const componentBlock = ` - ${componentTag} { - ${vars} +/** + * Components that render a child component inside their own shadow root. + * + * Token CSS is added to the document head, so its selectors can't reach an + * element inside a shadow root: `ion-button { … }` styles every `ion-button` + * in the page except the Cancel button that `ion-datetime` renders itself. + * Custom properties do inherit into a shadow root, though, so each host listed + * here joins the child's selectors and passes the tokens down: + * + * ```css + * ion-button, + * ion-datetime, + * ion-input-password-toggle { + * --ion-button-margin-top: 4px; + * } + * + * ion-datetime, + * ion-datetime ion-button { + * --ion-button-margin-top: 0px; + * } + * ``` + * + * The internal button inherits the value from its host, and its stylesheet reads it: + * + * ```html + * + * #shadow-root + * Cancel + * + * ``` + * + * ```css + * // button.scss, compiled + * :host { + * margin-top: var(--ion-button-margin-top); + * } + * ``` + * + * Add a component here when it starts rendering a migrated component in its + * shadow root, or that child renders with no tokens. A spec test checks this. + */ +export const INTERNAL_CHILD_COMPONENTS: Record = { + IonDatetime: ['IonButton'], + IonInputPasswordToggle: ['IonButton'], +}; + +/** + * Returns the tags of the components that render `component` in their shadow root. + */ +const getInternalRenderHosts = (component: string): string[] => + Object.entries(INTERNAL_CHILD_COMPONENTS) + .filter(([, children]) => children.includes(component)) + .map(([host]) => convertToKebabCase(host)); + +/** + * A key such as `IonButton` inside a component's tokens is a child component. + */ +const isChildComponentKey = (key: string) => /^Ion[A-Z]/.test(key); + +/** + * Separates a component's own tokens from the child components nested in them. + * Each child comes with the selector of the parent it is nested in. + * + * Children nested under `semantic` only apply when the parent has a color, so + * their parent selector gets the `.ion-color` class: + * + * ```ts + * IonToolbar: { + * semantic: { + * IonButtons: { IonButton: { … } }, + * }, + * } + * ``` + * + * Becomes `ion-toolbar.ion-color ion-buttons ion-button { … }`. Any other key + * under `semantic` stays one of the parent's own tokens. + */ +const splitChildComponents = (componentTokens: any, selector: string) => { + const ownTokens: Record = {}; + const children: [string, unknown, string][] = []; + + for (const [key, value] of Object.entries(componentTokens ?? {})) { + if (isChildComponentKey(key)) { + children.push([key, value, selector]); + } else if (key === 'semantic' && value !== null && typeof value === 'object') { + const semanticTokens: Record = {}; + + for (const [semanticKey, semanticValue] of Object.entries(value)) { + if (isChildComponentKey(semanticKey)) { + children.push([semanticKey, semanticValue, `${selector}.ion-color`]); + } else { + semanticTokens[semanticKey] = semanticValue; + } } - `; - css += componentBlock; + if (Object.keys(semanticTokens).length > 0) { + ownTokens[key] = semanticTokens; + } + } else { + ownTokens[key] = value; + } + } + + return { ownTokens, children }; +}; + +/** + * Generates the blocks for a child component nested in a parent's tokens, so the + * parent can theme the children it contains without new token names: + * + * ```ts + * IonDatetime: { + * IonButton: { margin: { top: '0px' } }, + * } + * ``` + * + * Becomes: + * + * ```css + * ion-datetime, + * ion-datetime ion-button { + * --ion-button-margin-top: 0px; + * } + * ``` + * + * The descendant selector reaches buttons in the light DOM, and the parent's + * own selector is added when it renders the child in its shadow root (refer to + * `INTERNAL_CHILD_COMPONENTS`). + */ +const generateChildBlocks = (parent: string, child: string, childTokens: any, parentSelector: string): string[] => { + const childTag = convertToKebabCase(child); + const selector = `${parentSelector} ${childTag}`; + const { ownTokens, children } = splitChildComponents(childTokens, selector); + + const blocks: string[] = []; + + if (Object.keys(ownTokens).length > 0) { + const rendersInternally = INTERNAL_CHILD_COMPONENTS[parent]?.includes(child) ?? false; + const selectors = rendersInternally ? [parentSelector, selector] : [selector]; + blocks.push(generateBlock(selectors, ownTokens, childTag)); } - return css; + for (const [grandchild, grandchildTokens, grandchildParentSelector] of children) { + blocks.push(...generateChildBlocks(child, grandchild, grandchildTokens, grandchildParentSelector)); + } + + return blocks; }; +/** + * Generates one block of `---` custom properties for a list of selectors. + */ +const generateBlock = (selectors: string[], tokens: Record, tag: string): string => ` + ${selectors.join(',\n ')} { + ${generateCSSVars(tokens, `--${tag}-`)} + } + `; + /** * Applies a component theme to an element if it exists in the custom theme * @param element The element to apply the theme to @@ -569,6 +748,19 @@ export const mix = (baseColor: string, mixColor: string, weight: string): string return `#${toHex(r)}${toHex(g)}${toHex(b)}`; }; +/** + * Lays a translucent color over a base color, for a `background` value. + * Renders the same as `color-mix()`, which is above Ionic's browser floor. + * + * @param base Opaque color underneath (e.g. `ionColor('primary', 'base')`) + * @param color Translucent color on top (e.g. `'rgba(255, 255, 255, 0.24)'`) + * + * @returns A `background` value (e.g. `'linear-gradient(rgba(...), rgba(...)) var(...)'`) + */ +export const overlay = (base: string, color: string): string => { + return `linear-gradient(${color}, ${color}) ${base}`; +}; + /** * Converts a string to kebab-case * diff --git a/packages/angular/src/lazy/directives/proxies.ts b/packages/angular/src/lazy/directives/proxies.ts index f3c4bee2c74..8f691ca8593 100644 --- a/packages/angular/src/lazy/directives/proxies.ts +++ b/packages/angular/src/lazy/directives/proxies.ts @@ -385,14 +385,14 @@ export declare interface IonBreadcrumbs extends Components.IonBreadcrumbs { @ProxyCmp({ - inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'theme', 'type'] + inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type'] }) @Component({ selector: 'ion-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'theme', 'type'], + inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'type'], outputs: ['ionFocus', 'ionBlur'], standalone: false }) diff --git a/packages/angular/src/standalone/directives/ion-button.ts b/packages/angular/src/standalone/directives/ion-button.ts index d5f9b45a65f..3e2d0a4c863 100644 --- a/packages/angular/src/standalone/directives/ion-button.ts +++ b/packages/angular/src/standalone/directives/ion-button.ts @@ -11,14 +11,14 @@ import { defineCustomElement as defineIonButton } from '@ionic/core/components/i @ProxyCmp({ defineCustomElementFn: defineIonButton, - inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'theme', 'type'] + inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type'] }) @Component({ selector: 'ion-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'theme', 'type'], + inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'type'], outputs: ['ionFocus', 'ionBlur'], }) export class IonButton { From 7ea4a968aab0c3da30eb917f69856e4129f279f3 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 21 Oct 2026 17:14:24 -0700 Subject: [PATCH 02/13] fix(button): keep datetime's ionic month buttons square --- core/src/components/datetime/datetime.ionic.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/components/datetime/datetime.ionic.scss b/core/src/components/datetime/datetime.ionic.scss index 3fbd6ea4723..f464d020950 100644 --- a/core/src/components/datetime/datetime.ionic.scss +++ b/core/src/components/datetime/datetime.ionic.scss @@ -136,6 +136,7 @@ --ion-button-hue-bold-clear-hover-background: #{globals.$ion-primitives-neutral-200}; --ion-button-hue-bold-clear-activated-background: #{globals.$ion-primitives-neutral-200}; --ion-button-hue-bold-clear-ripple-effect-color: #{globals.$ion-primitives-neutral-200}; + min-width: globals.$ion-scale-1000; min-height: globals.$ion-scale-1000; } From bf191d5d935516d262921a9cc56afbf882a0fd7f Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 22 Oct 2026 11:25:51 -0700 Subject: [PATCH 03/13] refactor(button): nest the min height and width tokens --- BREAKING.md | 4 ++-- .../src/components/button/button.interface.ts | 9 +++++--- core/src/themes/ionic/default.tokens.ts | 22 +++++++++++++------ core/src/themes/ios/default.tokens.ts | 16 ++++++++++---- core/src/themes/md/default.tokens.ts | 16 ++++++++++---- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index 4c4af559ba9..746fcab9488 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -55,8 +55,8 @@ The following breaking changes apply to `ion-button`: 1. The `border-radius` of the `ios` button is now `6px` at every size, instead of `6px`, `14px` and `16px` for the small, default and large sizes. The `md` button now defaults to `999px` instead of `4px`. Both follow the iOS and Material Design 3 guidelines. To revert to the previous appearance, set the `shape` to `"soft"` for `md`. For `ios`, set `IonButton.shape.soft.border.radius` for global styles or `--ion-button-shape-soft-border-radius` for component-specific styles to `14px`, and to `6px` and `16px` on small and large buttons. 2. Icon-only buttons in the `start` and `end` slots of `ion-input`, `ion-select` and `ion-textarea` now follow the button's `shape` instead of always being circles. For `ios`, set the `shape` to `"round"` to keep them circular. 3. The font size of `clear` buttons in the `ios` theme now matches the other fills at each size, instead of `17px` at the default size. To keep the previous size, set `--ion-button-size-medium-font-size` to `17px` on those buttons. -4. The `ios` button is now `28px`, `34px` and `50px` tall at the small, default and large sizes, with less padding, following the iOS guidelines. To revert to the previous appearance, set `IonButton.size..minHeight` for global styles or `--ion-button-size--min-height` for component-specific styles to `2.1em`, `3.1em` and `3.1em`, and the padding (`IonButton.size..padding` or `--ion-button-size--padding-*`) to `4px 0.9em`, `13px 1em` and `17px 1em`. -5. Icon-only buttons are now squares as tall as a text button of the same size. In `md`, this makes them a few pixels smaller at the small and default sizes and larger at the large size. To change their size, set the size's `minHeight` as described above. +4. The `ios` button is now `28px`, `34px` and `50px` tall at the small, default and large sizes, with less padding, following the iOS guidelines. To revert to the previous appearance, set `IonButton.size..min.height` for global styles or `--ion-button-size--min-height` for component-specific styles to `2.1em`, `3.1em` and `3.1em`, and the padding (`IonButton.size..padding` or `--ion-button-size--padding-*`) to `4px 0.9em`, `13px 1em` and `17px 1em`. +5. Icon-only buttons are now squares as tall as a text button of the same size. In `md`, this makes them a few pixels smaller at the small and default sizes and larger at the large size. To change their size, set the size's `min.height` as described above. 6. CSS variables have been replaced. [6](#version-10x-button-replaced-css-variables) 7. `--padding-*` CSS variables are no longer part of the documented public API (but remain functional). [7](#version-10x-button-padding-variables) 8. The fill, size, shape and expand classes have been renamed. [8](#version-10x-button-renamed-classes) diff --git a/core/src/components/button/button.interface.ts b/core/src/components/button/button.interface.ts index 0a4b642e344..1bf3788fffa 100644 --- a/core/src/components/button/button.interface.ts +++ b/core/src/components/button/button.interface.ts @@ -205,9 +205,12 @@ type IonButtonSizeDefinition = { height?: string | number; }; - /** Also the width and height of an icon-only button, so it's a square as tall as a text button */ - minHeight?: string | number; - minWidth?: string | number; + min?: { + /** Also the width and height of an icon-only button, so it's a square as tall as a text button */ + height?: string | number; + width?: string | number; + }; + padding?: IonPadding; icon?: IonButtonIconDefinition; diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index 78626e01b29..c4aff77554a 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -563,8 +563,10 @@ export const defaultTheme: DefaultTheme = { // Sizes size: { small: { - minHeight: 'var(--ion-scaling-lg)', - minWidth: 'var(--ion-scaling-1600)', + min: { + height: 'var(--ion-scaling-lg)', + width: 'var(--ion-scaling-1600)', + }, padding: { top: 'var(--ion-spacing-0)', @@ -670,8 +672,10 @@ export const defaultTheme: DefaultTheme = { }, medium: { - minHeight: 'var(--ion-scaling-xl)', - minWidth: 'var(--ion-scaling-xxxl)', + min: { + height: 'var(--ion-scaling-xl)', + width: 'var(--ion-scaling-xxxl)', + }, padding: { top: 'var(--ion-spacing-0)', @@ -777,8 +781,10 @@ export const defaultTheme: DefaultTheme = { }, large: { - minHeight: 'var(--ion-scaling-xxl)', - minWidth: 'var(--ion-scaling-xxxxl)', + min: { + height: 'var(--ion-scaling-xxl)', + width: 'var(--ion-scaling-xxxxl)', + }, padding: { top: 'var(--ion-spacing-0)', @@ -1059,7 +1065,9 @@ export const defaultTheme: DefaultTheme = { // Toolbar buttons are shorter than standalone ones; icon-only ones are squares of this height size: { medium: { - minHeight: 'var(--ion-scaling-lg)', + min: { + height: 'var(--ion-scaling-lg)', + }, }, }, }, diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index 8ed064ee432..4e5c43c0d18 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -538,7 +538,9 @@ export const defaultTheme: DefaultTheme = { // Sizes size: { small: { - minHeight: 'clamp(23px, 2.16em, 54px)', + min: { + height: 'clamp(23px, 2.16em, 54px)', + }, padding: { top: 'var(--ion-spacing-xxs)', @@ -596,7 +598,9 @@ export const defaultTheme: DefaultTheme = { }, medium: { - minHeight: 'clamp(30px, 2.125em, 60px)', + min: { + height: 'clamp(30px, 2.125em, 60px)', + }, padding: { top: '7px', @@ -654,7 +658,9 @@ export const defaultTheme: DefaultTheme = { }, large: { - minHeight: 'clamp(46px, 2.5em, 78px)', + min: { + height: 'clamp(46px, 2.5em, 78px)', + }, padding: { top: '14px', @@ -752,7 +758,9 @@ export const defaultTheme: DefaultTheme = { size: { medium: { - minHeight: 'var(--ion-scaling-md)', + min: { + height: 'var(--ion-scaling-md)', + }, padding: { top: 'var(--ion-spacing-75)', diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index 2c3fff483cb..915a056dcbb 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -558,7 +558,9 @@ export const defaultTheme: DefaultTheme = { // Sizes size: { small: { - minHeight: '2.1em', + min: { + height: '2.1em', + }, padding: { top: 'var(--ion-spacing-xxs)', @@ -613,7 +615,9 @@ export const defaultTheme: DefaultTheme = { }, medium: { - minHeight: 'var(--ion-scaling-900)', + min: { + height: 'var(--ion-scaling-900)', + }, padding: { top: 'var(--ion-spacing-sm)', @@ -671,7 +675,9 @@ export const defaultTheme: DefaultTheme = { }, large: { - minHeight: '2.8em', + min: { + height: '2.8em', + }, padding: { top: 'var(--ion-spacing-350)', @@ -766,7 +772,9 @@ export const defaultTheme: DefaultTheme = { size: { medium: { - minHeight: 'var(--ion-scaling-md)', + min: { + height: 'var(--ion-scaling-md)', + }, padding: { top: 'var(--ion-spacing-75)', From e2c3d408f8bec4b591495b82545288102ae74502 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 23 Oct 2026 20:05:24 -0700 Subject: [PATCH 04/13] fix(button): keep the default text color under the ripple --- core/src/components/button/button.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/components/button/button.scss b/core/src/components/button/button.scss index ff68a70b6b4..019f24c0a1e 100644 --- a/core/src/components/button/button.scss +++ b/core/src/components/button/button.scss @@ -389,14 +389,16 @@ $spinner-sizes: xsmall, small, medium, large, xlarge; /** * Only `md` mode renders a ripple. When it does, the ripple shows the press, - * so the button keeps its default background instead of the activated one. + * so the button keeps its default background and text color instead of the activated ones. */ :host(.button-hue-bold.button-fill-#{$fill}.ion-activated) .button-native:has(ion-ripple-effect) { background: var(#{$token}-default-background); + color: var(#{$token}-default-color); } :host(.button-hue-bold.button-fill-#{$fill}.ion-color.ion-activated) .button-native:has(ion-ripple-effect) { background: var(#{$token}-semantic-default-background); + color: var(#{$token}-semantic-default-color); } } From 066c5e58c5edac5579e049a1499827c5beec6f25 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 26 Oct 2026 16:24:08 -0700 Subject: [PATCH 05/13] refactor(button): remove the line height token --- core/api.txt | 3 --- core/src/components/button/_button.docs.scss | 3 --- core/src/components/button/button.interface.ts | 4 ---- core/src/components/button/button.scss | 2 -- core/src/themes/ionic/default.tokens.ts | 12 ------------ 5 files changed, 24 deletions(-) diff --git a/core/api.txt b/core/api.txt index 4236009ffa4..123405c1c71 100644 --- a/core/api.txt +++ b/core/api.txt @@ -574,7 +574,6 @@ ion-button,css-prop,--ion-button-size-large-icon-trailing-margin-bottom ion-button,css-prop,--ion-button-size-large-icon-trailing-margin-end ion-button,css-prop,--ion-button-size-large-icon-trailing-margin-start ion-button,css-prop,--ion-button-size-large-icon-trailing-margin-top -ion-button,css-prop,--ion-button-size-large-line-height ion-button,css-prop,--ion-button-size-large-min-height ion-button,css-prop,--ion-button-size-large-min-width ion-button,css-prop,--ion-button-size-large-padding-bottom @@ -614,7 +613,6 @@ ion-button,css-prop,--ion-button-size-medium-icon-trailing-margin-bottom ion-button,css-prop,--ion-button-size-medium-icon-trailing-margin-end ion-button,css-prop,--ion-button-size-medium-icon-trailing-margin-start ion-button,css-prop,--ion-button-size-medium-icon-trailing-margin-top -ion-button,css-prop,--ion-button-size-medium-line-height ion-button,css-prop,--ion-button-size-medium-min-height ion-button,css-prop,--ion-button-size-medium-min-width ion-button,css-prop,--ion-button-size-medium-padding-bottom @@ -654,7 +652,6 @@ ion-button,css-prop,--ion-button-size-small-icon-trailing-margin-bottom ion-button,css-prop,--ion-button-size-small-icon-trailing-margin-end ion-button,css-prop,--ion-button-size-small-icon-trailing-margin-start ion-button,css-prop,--ion-button-size-small-icon-trailing-margin-top -ion-button,css-prop,--ion-button-size-small-line-height ion-button,css-prop,--ion-button-size-small-min-height ion-button,css-prop,--ion-button-size-small-min-width ion-button,css-prop,--ion-button-size-small-padding-bottom diff --git a/core/src/components/button/_button.docs.scss b/core/src/components/button/_button.docs.scss index 58d9d03d87b..3248d71dd96 100644 --- a/core/src/components/button/_button.docs.scss +++ b/core/src/components/button/_button.docs.scss @@ -16,7 +16,6 @@ * @prop --ion-button-size-small-min-width: Minimum width of the `small` button size * @prop --ion-button-size-small-min-height: Minimum height of the `small` button size, and the width and height of an icon-only button of that size * @prop --ion-button-size-small-font-size: Font size of the `small` button size - * @prop --ion-button-size-small-line-height: Line height of the `small` button size * @prop --ion-button-size-small-padding-top: Top padding of the `small` button size * @prop --ion-button-size-small-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the `small` button size * @prop --ion-button-size-small-padding-bottom: Bottom padding of the `small` button size @@ -56,7 +55,6 @@ * @prop --ion-button-size-medium-min-width: Minimum width of the `medium` button size * @prop --ion-button-size-medium-min-height: Minimum height of the `medium` button size, and the width and height of an icon-only button of that size * @prop --ion-button-size-medium-font-size: Font size of the `medium` button size - * @prop --ion-button-size-medium-line-height: Line height of the `medium` button size * @prop --ion-button-size-medium-padding-top: Top padding of the `medium` button size * @prop --ion-button-size-medium-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the `medium` button size * @prop --ion-button-size-medium-padding-bottom: Bottom padding of the `medium` button size @@ -96,7 +94,6 @@ * @prop --ion-button-size-large-min-width: Minimum width of the `large` button size * @prop --ion-button-size-large-min-height: Minimum height of the `large` button size, and the width and height of an icon-only button of that size * @prop --ion-button-size-large-font-size: Font size of the `large` button size - * @prop --ion-button-size-large-line-height: Line height of the `large` button size * @prop --ion-button-size-large-padding-top: Top padding of the `large` button size * @prop --ion-button-size-large-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the `large` button size * @prop --ion-button-size-large-padding-bottom: Bottom padding of the `large` button size diff --git a/core/src/components/button/button.interface.ts b/core/src/components/button/button.interface.ts index 1bf3788fffa..0c8e952950e 100644 --- a/core/src/components/button/button.interface.ts +++ b/core/src/components/button/button.interface.ts @@ -201,10 +201,6 @@ type IonButtonSizeDefinition = { size?: string | number; }; - line?: { - height?: string | number; - }; - min?: { /** Also the width and height of an icon-only button, so it's a square as tall as a text button */ height?: string | number; diff --git a/core/src/components/button/button.scss b/core/src/components/button/button.scss index 019f24c0a1e..f267c538ca7 100644 --- a/core/src/components/button/button.scss +++ b/core/src/components/button/button.scss @@ -120,8 +120,6 @@ $spinner-sizes: xsmall, small, medium, large, xlarge; min-height: var(--ion-button-size-#{$size}-min-height); font-size: var(--ion-button-size-#{$size}-font-size); - - line-height: var(--ion-button-size-#{$size}-line-height); } :host(.button-size-#{$size}) .button-native { diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index c4aff77554a..750f4496868 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -579,10 +579,6 @@ export const defaultTheme: DefaultTheme = { size: dynamicFont(global.root, 14), }, - line: { - height: dynamicFont(global.root, 20), - }, - icon: { font: { size: dynamicFont(global.root, 20), @@ -688,10 +684,6 @@ export const defaultTheme: DefaultTheme = { size: dynamicFont(global.root, 16), }, - line: { - height: dynamicFont(global.root, 24), - }, - icon: { font: { size: dynamicFont(global.root, 20), @@ -797,10 +789,6 @@ export const defaultTheme: DefaultTheme = { size: dynamicFont(global.root, 20), }, - line: { - height: dynamicFont(global.root, 24), - }, - icon: { font: { size: dynamicFont(global.root, 24), From 8b7424087b3ad60e912d3ff7376b2360e6f71ec4 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 26 Oct 2026 19:11:23 -0700 Subject: [PATCH 06/13] chore(button): point the padding hook TODOs at each parent's migration --- core/src/components/button/button.scss | 3 --- core/src/components/buttons/buttons.common.scss | 1 + core/src/components/buttons/buttons.ionic.scss | 1 + core/src/components/buttons/buttons.ios.scss | 1 + core/src/components/buttons/buttons.md.scss | 1 + core/src/components/buttons/buttons.native.scss | 1 + core/src/components/datetime/datetime.ios.scss | 1 + core/src/components/datetime/datetime.md.scss | 1 + .../input-password-toggle/input-password-toggle.ionic.scss | 1 + core/src/components/input/input.ios.scss | 1 + core/src/components/input/input.md.scss | 1 + core/src/components/item/item.ios.scss | 1 + core/src/components/item/item.md.scss | 1 + core/src/components/list-header/list-header.ios.scss | 1 + core/src/components/select/select.ionic.scss | 1 + core/src/components/select/select.ios.scss | 1 + core/src/components/select/select.md.scss | 1 + core/src/components/textarea/textarea.ios.scss | 1 + core/src/components/textarea/textarea.md.scss | 1 + 19 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/src/components/button/button.scss b/core/src/components/button/button.scss index f267c538ca7..2e79c8ee2a9 100644 --- a/core/src/components/button/button.scss +++ b/core/src/components/button/button.scss @@ -13,9 +13,6 @@ $spinner-sizes: xsmall, small, medium, large, xlarge; /** * The undocumented padding hooks still work for the `ion-padding` classes set on * the button itself. Reset them so the button never inherits them from an ancestor. - * - * TODO(FW-6838): Parents such as ion-buttons, ion-item and ion-input still set these - * hooks on buttons. Move them to nested tokens, such as `IonItem.IonButton.size.small.padding`. */ --padding-top: initial; --padding-end: initial; diff --git a/core/src/components/buttons/buttons.common.scss b/core/src/components/buttons/buttons.common.scss index f2b2eaf6674..0eaea215a6f 100644 --- a/core/src/components/buttons/buttons.common.scss +++ b/core/src/components/buttons/buttons.common.scss @@ -11,6 +11,7 @@ // Toolbar Buttons // -------------------------------------------------- +// TODO(FW-6839): Move these button padding hooks to IonButtons.IonButton tokens ::slotted(*) ion-button { --padding-top: 0; --padding-bottom: 0; diff --git a/core/src/components/buttons/buttons.ionic.scss b/core/src/components/buttons/buttons.ionic.scss index 5cfff73f06d..5a5f834358a 100644 --- a/core/src/components/buttons/buttons.ionic.scss +++ b/core/src/components/buttons/buttons.ionic.scss @@ -4,6 +4,7 @@ // Ionic Buttons // -------------------------------------------------- +// TODO(FW-6839): Move these button padding hooks to IonButtons.IonButton tokens ::slotted(*) .button-has-icon-only { --padding-top: 0; --padding-bottom: 0; diff --git a/core/src/components/buttons/buttons.ios.scss b/core/src/components/buttons/buttons.ios.scss index 1371db2a668..d36a6f4743d 100644 --- a/core/src/components/buttons/buttons.ios.scss +++ b/core/src/components/buttons/buttons.ios.scss @@ -5,6 +5,7 @@ // -------------------------------------------------- // TODO(FW-6203): do we need these styles? +// TODO(FW-6839): Move these button padding hooks to IonButtons.IonButton tokens ::slotted(*) ion-button { --padding-top: 3px; --padding-bottom: 3px; diff --git a/core/src/components/buttons/buttons.md.scss b/core/src/components/buttons/buttons.md.scss index 7a4460f6930..27d2d1031de 100644 --- a/core/src/components/buttons/buttons.md.scss +++ b/core/src/components/buttons/buttons.md.scss @@ -5,6 +5,7 @@ // -------------------------------------------------- // TODO(FW-6203): do we need these styles? +// TODO(FW-6839): Move these button padding hooks to IonButtons.IonButton tokens ::slotted(*) ion-button { --padding-top: 3px; --padding-bottom: 3px; diff --git a/core/src/components/buttons/buttons.native.scss b/core/src/components/buttons/buttons.native.scss index 774af90a197..a63788bffbf 100644 --- a/core/src/components/buttons/buttons.native.scss +++ b/core/src/components/buttons/buttons.native.scss @@ -8,6 +8,7 @@ // Toolbar Buttons // -------------------------------------------------- +// TODO(FW-6839): Move these button padding hooks to IonButtons.IonButton tokens ::slotted(*) ion-button { --padding-top: 0; --padding-bottom: 0; diff --git a/core/src/components/datetime/datetime.ios.scss b/core/src/components/datetime/datetime.ios.scss index 5f5855964db..2d736ff20e1 100644 --- a/core/src/components/datetime/datetime.ios.scss +++ b/core/src/components/datetime/datetime.ios.scss @@ -73,6 +73,7 @@ } // These styles are copied from buttons.ios.scss +// TODO(FW-6893): Move these button padding hooks to IonDatetime.IonButton tokens .calendar-next-prev ion-button { --padding-top: 0; --padding-bottom: 0; diff --git a/core/src/components/datetime/datetime.md.scss b/core/src/components/datetime/datetime.md.scss index 6607ac2c5f0..4e0c96d0d9b 100644 --- a/core/src/components/datetime/datetime.md.scss +++ b/core/src/components/datetime/datetime.md.scss @@ -60,6 +60,7 @@ } // These styles are copied from buttons.md.scss +// TODO(FW-6893): Move these button padding hooks to IonDatetime.IonButton tokens .calendar-next-prev ion-button { --padding-top: 12px; --padding-end: 12px; diff --git a/core/src/components/input-password-toggle/input-password-toggle.ionic.scss b/core/src/components/input-password-toggle/input-password-toggle.ionic.scss index b0a3019a213..d41494c14f7 100644 --- a/core/src/components/input-password-toggle/input-password-toggle.ionic.scss +++ b/core/src/components/input-password-toggle/input-password-toggle.ionic.scss @@ -2,6 +2,7 @@ // Ionic Input Toggle Password // -------------------------------------------------- +// TODO(FW-6846): Move these button padding hooks to IonInputPasswordToggle.IonButton tokens :host ion-button { --margin-end: calc(-1 * var(--padding-end)); --padding-end: inherit; diff --git a/core/src/components/input/input.ios.scss b/core/src/components/input/input.ios.scss index 0925a33120d..91533b8570b 100644 --- a/core/src/components/input/input.ios.scss +++ b/core/src/components/input/input.ios.scss @@ -25,6 +25,7 @@ // Start/End Slots // ---------------------------------------------------------------- +// TODO(FW-6878): Move these button padding hooks to IonInput.IonButton tokens /** * Slotted buttons have a lot of default padding that can * cause them to look misaligned from other pieces such diff --git a/core/src/components/input/input.md.scss b/core/src/components/input/input.md.scss index fd2a4bf6c2b..ccbc3b6a75f 100644 --- a/core/src/components/input/input.md.scss +++ b/core/src/components/input/input.md.scss @@ -105,6 +105,7 @@ // Start/End Slots // ---------------------------------------------------------------- +// TODO(FW-6878): Move these button padding hooks to IonInput.IonButton tokens /** * Slotted buttons have a lot of default padding that can * cause them to look misaligned from other pieces such diff --git a/core/src/components/item/item.ios.scss b/core/src/components/item/item.ios.scss index 8071122da57..6e65df36725 100644 --- a/core/src/components/item/item.ios.scss +++ b/core/src/components/item/item.ios.scss @@ -138,6 +138,7 @@ // iOS Item Button // -------------------------------------------------- +// TODO(FW-6847): Move these button padding hooks to IonItem.IonButton tokens ::slotted(.button-size-small) { --padding-top: 1px; --padding-bottom: 1px; diff --git a/core/src/components/item/item.md.scss b/core/src/components/item/item.md.scss index 3114967ed38..31a3db03117 100644 --- a/core/src/components/item/item.md.scss +++ b/core/src/components/item/item.md.scss @@ -236,6 +236,7 @@ // Material Design Item Button // -------------------------------------------------- +// TODO(FW-6847): Move these button padding hooks to IonItem.IonButton tokens ::slotted(.button-size-small) { --padding-top: 2px; --padding-bottom: 2px; diff --git a/core/src/components/list-header/list-header.ios.scss b/core/src/components/list-header/list-header.ios.scss index c1033cc7c17..40c0463f95d 100644 --- a/core/src/components/list-header/list-header.ios.scss +++ b/core/src/components/list-header/list-header.ios.scss @@ -39,6 +39,7 @@ @include margin(29px, null, 6px); } +// TODO(FW-6852): Move these button padding hooks to IonListHeader.IonButton tokens ::slotted(ion-button) { --padding-top: 0; --padding-bottom: 0; diff --git a/core/src/components/select/select.ionic.scss b/core/src/components/select/select.ionic.scss index ce77a6bf96c..e52246f4dd3 100644 --- a/core/src/components/select/select.ionic.scss +++ b/core/src/components/select/select.ionic.scss @@ -391,6 +391,7 @@ margin-inline-start: globals.$ion-space-200; } +// TODO(FW-6881): Move these button padding hooks to IonSelect.IonButton tokens ::slotted(ion-button[slot="start"].button-has-icon-only), ::slotted(ion-button[slot="end"].button-has-icon-only) { --padding-start: #{globals.$ion-space-200}; diff --git a/core/src/components/select/select.ios.scss b/core/src/components/select/select.ios.scss index 91b12b2beb8..acefe39a192 100644 --- a/core/src/components/select/select.ios.scss +++ b/core/src/components/select/select.ios.scss @@ -32,6 +32,7 @@ // Start/End Slots // ---------------------------------------------------------------- +// TODO(FW-6881): Move these button padding hooks to IonSelect.IonButton tokens /** * Slotted buttons have a lot of default padding that can * cause them to look misaligned from other pieces such diff --git a/core/src/components/select/select.md.scss b/core/src/components/select/select.md.scss index 1b9d6bfe75b..15f33bb9510 100644 --- a/core/src/components/select/select.md.scss +++ b/core/src/components/select/select.md.scss @@ -131,6 +131,7 @@ // Start/End Slots // ---------------------------------------------------------------- +// TODO(FW-6881): Move these button padding hooks to IonSelect.IonButton tokens /** * Slotted buttons have a lot of default padding that can * cause them to look misaligned from other pieces such diff --git a/core/src/components/textarea/textarea.ios.scss b/core/src/components/textarea/textarea.ios.scss index ff84943f18b..f9304fd9345 100644 --- a/core/src/components/textarea/textarea.ios.scss +++ b/core/src/components/textarea/textarea.ios.scss @@ -54,6 +54,7 @@ margin-top: 10px; } +// TODO(FW-6880): Move these button padding hooks to IonTextarea.IonButton tokens /** * Slotted buttons have a lot of default padding that can * cause them to look misaligned from other pieces such diff --git a/core/src/components/textarea/textarea.md.scss b/core/src/components/textarea/textarea.md.scss index 3db578635fe..cbc2a64f351 100644 --- a/core/src/components/textarea/textarea.md.scss +++ b/core/src/components/textarea/textarea.md.scss @@ -106,6 +106,7 @@ margin-top: 7px; } +// TODO(FW-6880): Move these button padding hooks to IonTextarea.IonButton tokens /** * Slotted buttons have a lot of default padding that can * cause them to look misaligned from other pieces such From e9734babe8ebc0de713bc46184ae14b52e6ca63d Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 26 Oct 2026 19:15:34 -0700 Subject: [PATCH 07/13] docs(button): explain the padding hook reset --- core/src/components/button/button.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/components/button/button.scss b/core/src/components/button/button.scss index 2e79c8ee2a9..3a9294d0f6d 100644 --- a/core/src/components/button/button.scss +++ b/core/src/components/button/button.scss @@ -11,8 +11,9 @@ $spinner-sizes: xsmall, small, medium, large, xlarge; :host { /** - * The undocumented padding hooks still work for the `ion-padding` classes set on - * the button itself. Reset them so the button never inherits them from an ancestor. + * Ancestors such as `.ion-padding` containers set `--padding-*` for themselves, + * and the button would inherit them. The reset limits them to values set on + * the button, such as its own `ion-padding` classes. */ --padding-top: initial; --padding-end: initial; From d377d8849d6c042739112db1b19ff7d72a96cd5c Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 27 Oct 2026 16:05:14 -0700 Subject: [PATCH 08/13] feat(button): render the ripple from the rippleEffect config The ripple follows the theme instead of the mode, and ionic sets rippleEffect to match md. Button falls back to the mode until FW-7893 makes the base theme's config always apply. --- core/src/components/button/button.tsx | 4 +- .../src/components/button/test/button.spec.ts | 46 +++++++++++++++++++ core/src/themes/ionic/default.tokens.ts | 1 + 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx index e3f1b33aac8..009a5d5dc81 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -461,6 +461,8 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf strong, } = this; const mode = getIonMode(this); + // TODO(FW-7893): remove the mode fallback once the base theme's config always applies + const showRipple = config.getBoolean('rippleEffect', mode === 'md'); const inheritedAttributes = this.ariaController?.attributes ?? {}; const TagType = href === undefined ? 'button' : ('a' as any); const attrs = @@ -523,7 +525,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf - {mode === 'md' && } + {showRipple && } ); diff --git a/core/src/components/button/test/button.spec.ts b/core/src/components/button/test/button.spec.ts index eac0a0732d6..73ccd4ea343 100644 --- a/core/src/components/button/test/button.spec.ts +++ b/core/src/components/button/test/button.spec.ts @@ -194,3 +194,49 @@ describe('button: hue', () => { expect(page.root!.classList.contains('button-hue-subtle')).toBe(true); }); }); + +describe('button: ripple effect', () => { + afterEach(() => { + config.reset({}); + }); + + it('should render a ripple in md mode when the theme config has no rippleEffect', async () => { + const page = await newSpecPage({ + components: [Button], + html: `Button`, + }); + + expect(page.root!.shadowRoot!.querySelector('ion-ripple-effect')).not.toBeNull(); + }); + + it('should not render a ripple in ios mode when the theme config has no rippleEffect', async () => { + const page = await newSpecPage({ + components: [Button], + html: `Button`, + }); + + expect(page.root!.shadowRoot!.querySelector('ion-ripple-effect')).toBeNull(); + }); + + it('should render a ripple in ios mode when rippleEffect is enabled', async () => { + config.reset({ rippleEffect: true }); + + const page = await newSpecPage({ + components: [Button], + html: `Button`, + }); + + expect(page.root!.shadowRoot!.querySelector('ion-ripple-effect')).not.toBeNull(); + }); + + it('should not render a ripple in md mode when rippleEffect is disabled', async () => { + config.reset({ rippleEffect: false }); + + const page = await newSpecPage({ + components: [Button], + html: `Button`, + }); + + expect(page.root!.shadowRoot!.querySelector('ion-ripple-effect')).toBeNull(); + }); +}); diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index 750f4496868..a1afad6cbf8 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -19,6 +19,7 @@ export const defaultTheme: DefaultTheme = { config: { formHighlight: true, + rippleEffect: true, components: { IonBadge: { From f475ea42305a48f9bb7818255810a61eb39c4c80 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 27 Oct 2026 16:09:05 -0700 Subject: [PATCH 09/13] test(button): note the ripple depends on the rippleEffect config --- core/scripts/testing/scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scripts/testing/scripts.js b/core/scripts/testing/scripts.js index b58b18197f8..28a8c10c9f6 100644 --- a/core/scripts/testing/scripts.js +++ b/core/scripts/testing/scripts.js @@ -258,7 +258,7 @@ const DEFAULT_PALETTE = 'light'; elements.map(async (el) => { await el.componentOnReady?.(); - // Only the `md` mode renders a ripple + // Skip elements that don't render a ripple, which depends on the `rippleEffect` config const ripple = el.shadowRoot?.querySelector('ion-ripple-effect') ?? el.querySelector('ion-ripple-effect'); if (!ripple) { return; From 4c8ed2cfb55dc3b599daa7ca0abcb2da606b6571 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 27 Oct 2026 16:39:01 -0700 Subject: [PATCH 10/13] test(button): loop over the interface arrays --- core/src/components/button/button.interface.ts | 2 +- .../src/components/button/test/badge/button.e2e.ts | 5 ++--- .../src/components/button/test/shape/button.e2e.ts | 14 +++++--------- .../components/button/test/states/button.e2e.ts | 8 ++++---- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/core/src/components/button/button.interface.ts b/core/src/components/button/button.interface.ts index 0c8e952950e..661a70c95c2 100644 --- a/core/src/components/button/button.interface.ts +++ b/core/src/components/button/button.interface.ts @@ -1,7 +1,7 @@ import type { Hue, IonMargin, IonPadding } from '../../themes/themes.interfaces'; import type { SpinnerSize } from '../spinner/spinner.interfaces'; -export const ION_BUTTON_FILLS = ['clear', 'outline', 'solid'] as const; +export const ION_BUTTON_FILLS = ['solid', 'outline', 'clear'] as const; export type IonButtonFill = (typeof ION_BUTTON_FILLS)[number]; export const ION_BUTTON_SHAPES = ['crisp', 'soft', 'round', 'rectangular'] as const; diff --git a/core/src/components/button/test/badge/button.e2e.ts b/core/src/components/button/test/badge/button.e2e.ts index a676d4b6121..12725cfa31b 100644 --- a/core/src/components/button/test/badge/button.e2e.ts +++ b/core/src/components/button/test/badge/button.e2e.ts @@ -2,8 +2,7 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; import { ION_BADGE_SIZES, ION_BADGE_VERTICAL_POSITIONS } from '../../../badge/badge.interfaces'; - -const BUTTON_SIZES = ['small', 'medium', 'large']; +import { ION_BUTTON_SIZES } from '../../button.interface'; const contents = ['', '1', '999+', '']; @@ -29,7 +28,7 @@ const renderRows = (buttonSize: string, buttonContent: string) => configs({ modes: ['md', 'ios', 'ionic-md'] }).forEach(({ config, screenshot, title }) => { test.describe(title('button: badge'), () => { test('should not have visual regressions with badges', async ({ page }) => { - const sections = BUTTON_SIZES.map( + const sections = ION_BUTTON_SIZES.map( (buttonSize) => `

button size: ${buttonSize}

${renderRows(buttonSize, '')} diff --git a/core/src/components/button/test/shape/button.e2e.ts b/core/src/components/button/test/shape/button.e2e.ts index f1e04988636..4b1b4472803 100644 --- a/core/src/components/button/test/shape/button.e2e.ts +++ b/core/src/components/button/test/shape/button.e2e.ts @@ -1,12 +1,7 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; -import { ION_BUTTON_SHAPES } from '../../button.interface'; - -// `solid` is the default fill, so it renders without a `fill` attribute -const FILLS = ['solid', 'outline', 'clear']; - -const SIZES = ['small', 'medium', 'large']; +import { ION_BUTTON_FILLS, ION_BUTTON_SHAPES, ION_BUTTON_SIZES } from '../../button.interface'; /** * This behavior does not vary across directions. @@ -15,7 +10,8 @@ configs({ modes: ['ionic-md', 'md', 'ios'], directions: ['ltr'] }).forEach(({ ti test.describe(title('button: shape'), () => { test('should not have visual regressions', async ({ page }) => { const grids = ION_BUTTON_SHAPES.flatMap((shape) => - FILLS.map((fill) => { + ION_BUTTON_FILLS.map((fill) => { + // `solid` is the default fill, so it renders without a `fill` attribute const fillAttr = fill === 'solid' ? '' : ` fill="${fill}"`; // Clear buttons have no background, so focus them to show the shape const focusedAttr = fill === 'clear' ? ' class="ion-focused"' : ''; @@ -24,8 +20,8 @@ configs({ modes: ['ionic-md', 'md', 'ios'], directions: ['ltr'] }).forEach(({ ti return `
- ${SIZES.map((size) => `Button`).join('')} - ${SIZES.map( + ${ION_BUTTON_SIZES.map((size) => `Button`).join('')} + ${ION_BUTTON_SIZES.map( (size) => ` diff --git a/core/src/components/button/test/states/button.e2e.ts b/core/src/components/button/test/states/button.e2e.ts index 6426299e56c..d68676545f0 100644 --- a/core/src/components/button/test/states/button.e2e.ts +++ b/core/src/components/button/test/states/button.e2e.ts @@ -1,16 +1,16 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; -const STATES = ['activated', 'focused']; +import { ION_BUTTON_FILLS } from '../../button.interface'; -// `solid` is the default fill, so it renders without a `fill` attribute -const FILLS = ['solid', 'outline', 'clear']; +const STATES = ['activated', 'focused']; configs({ directions: ['ltr'], modes: ['ionic-md', 'md', 'ios'] }).forEach(({ title, screenshot, config }) => { test.describe(title('button: states'), () => { STATES.forEach((state) => { test(`should render ${state} buttons`, async ({ page }) => { - const buttons = FILLS.map((fill) => { + const buttons = ION_BUTTON_FILLS.map((fill) => { + // `solid` is the default fill, so it renders without a `fill` attribute const fillAttr = fill === 'solid' ? '' : ` fill="${fill}"`; return ` From 3945d12988640d3e9b9173d0810ef781febf3417 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 27 Oct 2026 17:02:44 -0700 Subject: [PATCH 11/13] style(buttons, datetime): remove comments about where moved rules went --- core/src/components/buttons/buttons.ionic.scss | 2 -- core/src/components/buttons/buttons.ios.scss | 2 -- core/src/components/buttons/buttons.md.scss | 2 -- core/src/components/datetime/datetime.ionic.scss | 6 +++--- core/src/components/datetime/datetime.md.scss | 6 +++--- core/src/components/datetime/datetime.native.scss | 6 +++--- 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/core/src/components/buttons/buttons.ionic.scss b/core/src/components/buttons/buttons.ionic.scss index 5a5f834358a..93f5975d435 100644 --- a/core/src/components/buttons/buttons.ionic.scss +++ b/core/src/components/buttons/buttons.ionic.scss @@ -10,8 +10,6 @@ --padding-bottom: 0; } -// Clear button colors are in the `IonToolbar.IonButtons.IonButton` tokens - // Toolbar Button Icon // -------------------------------------------------- diff --git a/core/src/components/buttons/buttons.ios.scss b/core/src/components/buttons/buttons.ios.scss index d36a6f4743d..8e118da0b93 100644 --- a/core/src/components/buttons/buttons.ios.scss +++ b/core/src/components/buttons/buttons.ios.scss @@ -22,8 +22,6 @@ --padding-bottom: 0; } -// Colors, shapes and fonts for the buttons are in the `IonToolbar.IonButtons.IonButton` tokens - // iOS Toolbar Button Icon // -------------------------------------------------- diff --git a/core/src/components/buttons/buttons.md.scss b/core/src/components/buttons/buttons.md.scss index 27d2d1031de..4c711485cd4 100644 --- a/core/src/components/buttons/buttons.md.scss +++ b/core/src/components/buttons/buttons.md.scss @@ -22,8 +22,6 @@ --padding-bottom: 0; } -// Colors, shapes and fonts for the buttons are in the `IonToolbar.IonButtons.IonButton` tokens - // Material Design Toolbar Icon Only Clear Button // -------------------------------------------------- diff --git a/core/src/components/datetime/datetime.ionic.scss b/core/src/components/datetime/datetime.ionic.scss index f464d020950..0afcce77e14 100644 --- a/core/src/components/datetime/datetime.ionic.scss +++ b/core/src/components/datetime/datetime.ionic.scss @@ -123,9 +123,9 @@ } /** - * TODO(FW-6893): Move to datetime's tokens in its migration. Button's migration only - * renamed these properties: a nested `IonDatetime.IonButton` token can't target just - * the previous and next buttons inside datetime's shadow root. + * TODO(FW-6893): Move to datetime's tokens in its migration. A nested + * `IonDatetime.IonButton` token can't target just the previous and next buttons inside + * datetime's shadow root. */ // TODO(ROU-11119): Rewrite this when we replace the ion-button with a native button .calendar-next-prev ion-button.button-has-icon-only { diff --git a/core/src/components/datetime/datetime.md.scss b/core/src/components/datetime/datetime.md.scss index 4e0c96d0d9b..fba114927ec 100644 --- a/core/src/components/datetime/datetime.md.scss +++ b/core/src/components/datetime/datetime.md.scss @@ -48,9 +48,9 @@ // ----------------------------------- /** - * TODO(FW-6893): Move to datetime's tokens in its migration. Button's migration only - * renamed these properties: a nested `IonDatetime.IonButton` token can't target just - * the month navigation buttons inside datetime's shadow root. + * TODO(FW-6893): Move to datetime's tokens in its migration. A nested + * `IonDatetime.IonButton` token can't target just the month navigation buttons inside + * datetime's shadow root. */ :host .calendar-action-buttons ion-button { --ion-button-hue-bold-clear-default-color: #{$text-color-step-350}; diff --git a/core/src/components/datetime/datetime.native.scss b/core/src/components/datetime/datetime.native.scss index 8479563af93..2c15fd3855d 100644 --- a/core/src/components/datetime/datetime.native.scss +++ b/core/src/components/datetime/datetime.native.scss @@ -125,9 +125,9 @@ } /** - * TODO(FW-6893): Move to datetime's tokens in its migration. Button's migration only - * renamed these properties: a nested `IonDatetime.IonButton` token can't target just - * the action buttons inside datetime's shadow root. + * TODO(FW-6893): Move to datetime's tokens in its migration. A nested + * `IonDatetime.IonButton` token can't target just the action buttons inside datetime's + * shadow root. */ // Default text color for action buttons (cancel, clear, done). // When datetime doesn't receive a color prop. From 066e21b8dfdd5f56031ea6d97ff7ed831283315f Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 28 Oct 2026 10:08:52 -0700 Subject: [PATCH 12/13] fix(buttons): keep the toolbar color on back and menu buttons --- core/src/components/buttons/buttons.ios.scss | 10 ++++++++++ core/src/components/buttons/buttons.md.scss | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/core/src/components/buttons/buttons.ios.scss b/core/src/components/buttons/buttons.ios.scss index 8e118da0b93..1781087e8a7 100644 --- a/core/src/components/buttons/buttons.ios.scss +++ b/core/src/components/buttons/buttons.ios.scss @@ -22,6 +22,16 @@ --padding-bottom: 0; } +// iOS Toolbar with Color: Back and Menu Buttons +// -------------------------------------------------- + +// TODO(FW-6835, FW-6853): Move to tokens when back button and menu button migrate +:host-context(.ion-color)::slotted(*) ion-back-button, +:host-context(.ion-color)::slotted(*) ion-menu-button { + --color: initial; + --background-focused: #{current-color(contrast)}; +} + // iOS Toolbar Button Icon // -------------------------------------------------- diff --git a/core/src/components/buttons/buttons.md.scss b/core/src/components/buttons/buttons.md.scss index 4c711485cd4..a0b584420f3 100644 --- a/core/src/components/buttons/buttons.md.scss +++ b/core/src/components/buttons/buttons.md.scss @@ -22,6 +22,24 @@ --padding-bottom: 0; } +// Material Design Toolbar Back and Menu Buttons +// -------------------------------------------------- + +// TODO(FW-6835, FW-6853): Move to tokens when back button and menu button migrate +::slotted(*) ion-back-button, +::slotted(*) ion-menu-button { + --background-hover: currentColor; +} + +:host-context(.ion-color)::slotted(*) ion-back-button, +:host-context(.ion-color)::slotted(*) ion-menu-button { + --color: initial; + --color-focused: #{current-color(contrast)}; + --color-hover: #{current-color(contrast)}; + --background-focused: #{current-color(contrast)}; + --background-hover: #{current-color(contrast)}; +} + // Material Design Toolbar Icon Only Clear Button // -------------------------------------------------- From 5b4bb1044b2d3907ff3533c49e4eab57b76e5513 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 28 Oct 2026 19:29:59 -0700 Subject: [PATCH 13/13] test(button): remove duplicate blank line from the basic test page --- core/src/components/button/test/basic/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/components/button/test/basic/index.html b/core/src/components/button/test/basic/index.html index 8825e8f8e06..726aaf2c138 100644 --- a/core/src/components/button/test/basic/index.html +++ b/core/src/components/button/test/basic/index.html @@ -97,7 +97,6 @@ Opacity: 0.2

-

Toggle Disabled