diff --git a/BREAKING.md b/BREAKING.md index f9e61176941..746fcab9488 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..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) +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..1bf3788fffa --- /dev/null +++ b/core/src/components/button/button.interface.ts @@ -0,0 +1,232 @@ +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; + }; + + 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; + 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..f464d020950 100644 --- a/core/src/components/datetime/datetime.ionic.scss +++ b/core/src/components/datetime/datetime.ionic.scss @@ -122,12 +122,21 @@ 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-width: globals.$ion-scale-1000; 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..c4aff77554a 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,779 @@ 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: { + min: { + height: 'var(--ion-scaling-lg)', + width: '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: { + min: { + height: 'var(--ion-scaling-xl)', + width: '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: { + min: { + height: 'var(--ion-scaling-xxl)', + width: '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: { + min: { + height: '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..4e5c43c0d18 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,764 @@ 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: { + min: { + height: '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: { + min: { + height: '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: { + min: { + height: '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: { + min: { + height: '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..915a056dcbb 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,771 @@ 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: { + min: { + height: '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: { + min: { + height: '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: { + min: { + height: '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: { + min: { + height: '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 {