fix(item): add input highlight using an absolute div (#15856)

Adds the following CSS properties to item:

```
--highlight-color-focused
--highlight-color-valid
--highlight-color-invalid
--highlight-height
```

This also fixes an issue where we were showing the highlight on items
with no lines, and shows inset vs full properly. Adds documentation and tests for input focus.

fixes #14036 fixes #9639 fixes #14952 closes #15690
This commit is contained in:
Brandy Carney
2018-10-05 16:15:39 -04:00
committed by GitHub
parent 23df042d78
commit f885f7d10a
12 changed files with 266 additions and 90 deletions

View File

@ -363,12 +363,12 @@ export class Input {
}
export declare interface Item extends StencilComponents<'IonItem'> {}
@Component({ selector: 'ion-item', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['color', 'mode', 'button', 'detail', 'detailIcon', 'disabled', 'href', 'lines', 'routerDirection', 'state', 'type'] })
@Component({ selector: 'ion-item', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['color', 'mode', 'button', 'detail', 'detailIcon', 'disabled', 'href', 'lines', 'routerDirection', 'type'] })
export class Item {
constructor(r: ElementRef) {
const el = r.nativeElement;
proxyInputs(this, el, ['color', 'mode', 'button', 'detail', 'detailIcon', 'disabled', 'href', 'lines', 'routerDirection', 'state', 'type']);
proxyInputs(this, el, ['color', 'mode', 'button', 'detail', 'detailIcon', 'disabled', 'href', 'lines', 'routerDirection', 'type']);
}
}

View File

@ -2107,7 +2107,6 @@ export namespace Components {
* When using a router, it specifies the transition direction when navigating to another page using `href`.
*/
'routerDirection'?: RouterDirection;
'state'?: 'valid' | 'invalid' | 'focus';
/**
* The type of the button. Only used when an `onclick` or `button` property is present. Possible values are: `"submit"`, `"reset"` and `"button"`. Default value is: `"button"`
*/
@ -2150,7 +2149,6 @@ export namespace Components {
* When using a router, it specifies the transition direction when navigating to another page using `href`.
*/
'routerDirection'?: RouterDirection;
'state'?: 'valid' | 'invalid' | 'focus';
/**
* The type of the button. Only used when an `onclick` or `button` property is present. Possible values are: `"submit"`, `"reset"` and `"button"`. Default value is: `"button"`
*/
@ -4017,7 +4015,7 @@ export namespace Components {
* The text to display on the ok button. Default: `OK`.
*/
'okText': string;
'open': (ev?: UIEvent | undefined) => Promise<HTMLIonActionSheetElement | HTMLIonAlertElement | HTMLIonPopoverElement>;
'open': (ev?: UIEvent | undefined) => Promise<HTMLIonAlertElement | HTMLIonPopoverElement | HTMLIonActionSheetElement>;
/**
* The text to display when the select is empty.
*/

View File

@ -324,7 +324,6 @@ export class Input implements ComponentInterface {
return {
class: {
...createColorClasses(this.color),
'in-item': hostContext('ion-item', this.el),
'has-value': this.hasValue(),
'has-focus': this.hasFocus

View File

@ -5,6 +5,33 @@ it('input: basic', async () => {
url: '/src/components/input/test/basic?ionic:animated=false'
});
const compare = await page.compareScreenshot();
let compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
const fullInput = await page.find('#fullInput');
await fullInput.click();
const fullItem = await page.find('#fullItem');
expect(fullItem).toHaveClass('item-has-focus');
compare = await page.compareScreenshot('full input focused');
expect(compare).toMatchScreenshot();
const insetInput = await page.find('#insetInput');
await insetInput.click();
const insetItem = await page.find('#insetItem');
expect(insetItem).toHaveClass('item-has-focus');
compare = await page.compareScreenshot('inset input focused');
expect(compare).toMatchScreenshot();
const noneInput = await page.find('#noneInput');
await noneInput.click();
const noneItem = await page.find('#noneItem');
expect(noneItem).toHaveClass('item-has-focus');
compare = await page.compareScreenshot('no lines input focused');
expect(compare).toMatchScreenshot();
});

View File

@ -28,6 +28,18 @@
<ion-input placeholder="Placeholder"></ion-input>
</ion-item>
<ion-item lines="full" id="fullItem">
<ion-input id="fullInput" placeholder="Full"></ion-input>
</ion-item>
<ion-item lines="inset" id="insetItem">
<ion-input id="insetInput" placeholder="Inset"></ion-input>
</ion-item>
<ion-item lines="none" id="noneItem">
<ion-input id="noneInput" placeholder="None"></ion-input>
</ion-item>
<ion-item>
<ion-label>Default Label</ion-label>
<ion-input value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"></ion-input>

View File

@ -9,16 +9,17 @@
--padding-start: #{$item-ios-padding-start};
--inner-padding-end: #{$item-ios-padding-end / 2};
--inner-border-width: #{0px 0px $item-ios-border-bottom-width 0px};
font-size: $item-ios-font-size;
}
:host(:not(.ion-color)) {
--background: var(--ion-item-background-color, transparent);
--background-activated: #{$item-ios-background-color-active};
--border-color: #{$item-ios-border-bottom-color};
--color: #{$item-ios-text-color};
--detail-icon-color: #{$item-ios-border-bottom-color};
--highlight-height: 0;
--highlight-color-focused: #{$item-ios-input-highlight-color};
--highlight-color-valid: #{$item-ios-input-highlight-color-valid};
--highlight-color-invalid: #{$item-ios-input-highlight-color-invalid};
font-size: $item-ios-font-size;
}
:host(.activated) {
@ -29,14 +30,24 @@
// iOS Item Lines
// --------------------------------------------------
// Default input items have an inset border
:host(.item-interactive) {
--show-full-highlight: 0;
--show-inset-highlight: 1;
}
// Full lines - apply the border to the item
// Inset lines - apply the border to the item inner
:host(.item-lines-full) {
--border-width: #{0px 0px $item-ios-border-bottom-width 0px};
--show-full-highlight: 1;
--show-inset-highlight: 0;
}
:host(.item-lines-inset) {
--inner-border-width: #{0px 0px $item-ios-border-bottom-width 0px};
--show-full-highlight: 0;
--show-inset-highlight: 1;
}
// Full lines - remove the border from the item inner (inset list items)
@ -45,11 +56,13 @@
:host(.item-lines-inset),
:host(.item-lines-none) {
--border-width: 0px;
--show-full-highlight: 0;
}
:host(.item-lines-full),
:host(.item-lines-none) {
--inner-border-width: 0px;
--show-inset-highlight: 0;
}

View File

@ -63,6 +63,15 @@ $item-ios-border-bottom-color: $item-ios-border-color !default;
/// @prop - Border bottom for the item
$item-ios-border-bottom: $item-ios-border-bottom-width $item-ios-border-bottom-style $item-ios-border-bottom-color !default;
/// @prop - Color of the item input highlight
$item-ios-input-highlight-color: ion-color(primary, base) !default;
/// @prop - Color of the item input highlight when valid
$item-ios-input-highlight-color-valid: ion-color(success, base) !default;
/// @prop - Color of the item input highlight when invalid
$item-ios-input-highlight-color-invalid: ion-color(danger, base) !default;
// Item Slots
// --------------------------------------------------

View File

@ -8,8 +8,17 @@
:host {
--transition: background-color 300ms cubic-bezier(.4, 0, .2, 1);
--padding-start: #{$item-md-padding-start};
--background: var(--ion-item-background-color, transparent);
--background-activated: #{$item-md-background-color-active};
--color: #{$item-md-text-color};
--border-color: #{$item-md-border-bottom-color};
--inner-padding-end: #{$item-md-padding-end / 2};
--padding-start: #{$item-md-padding-start};
--inner-border-width: #{0 0 $item-md-border-bottom-width 0};
--detail-icon-color: #{$item-md-border-bottom-color};
--highlight-height: 2px;
--highlight-color-focused: #{$item-md-input-highlight-color};
--highlight-color-valid: #{$item-md-input-highlight-color-valid};
--highlight-color-invalid: #{$item-md-input-highlight-color-invalid};
font-size: $item-md-font-size;
font-weight: normal;
@ -17,31 +26,30 @@
text-transform: none;
}
:host(:not(.ion-color)) {
--background: var(--ion-item-background-color, transparent);
--background-activated: #{$item-md-background-color-active};
--border-color: #{$item-md-border-bottom-color};
--color: #{$item-md-text-color};
--detail-icon-color: #{$item-md-border-bottom-color};
}
// Material Design Item Lines
// --------------------------------------------------
// Default input items have a border
// Default input items have a full border
:host(.item-interactive) {
--border-width: #{0 0 $item-md-border-bottom-width 0};
--inner-border-width: 0;
--show-full-highlight: 1;
--show-inset-highlight: 0;
}
// Full lines - apply the border to the item
// Inset lines - apply the border to the item inner
:host(.item-lines-full) {
--border-width: #{0 0 $item-md-border-bottom-width 0};
--show-full-highlight: 1;
--show-inset-highlight: 0;
}
:host(.item-lines-inset) {
--inner-border-width: #{0 0 $item-md-border-bottom-width 0};
--show-full-highlight: 0;
--show-inset-highlight: 1;
}
// Full lines - remove the border from the item inner (inset list items)
@ -50,11 +58,13 @@
:host(.item-lines-inset),
:host(.item-lines-none) {
--border-width: 0;
--show-full-highlight: 0;
}
:host(.item-lines-full),
:host(.item-lines-none) {
--inner-border-width: 0;
--show-inset-highlight: 0;
}
// Material Design Item Detail Push

View File

@ -48,6 +48,15 @@ $item-md-border-bottom-color: $item-md-border-color !default;
/// @prop - Border bottom for the item when lines are displayed
$item-md-border-bottom: $item-md-border-bottom-width $item-md-border-bottom-style $item-md-border-color !default;
/// @prop - Color of the item input highlight
$item-md-input-highlight-color: ion-color(primary, base) !default;
/// @prop - Color of the item input highlight when valid
$item-md-input-highlight-color-valid: ion-color(success, base) !default;
/// @prop - Color of the item input highlight when invalid
$item-md-input-highlight-color-invalid: ion-color(danger, base) !default;
// Item Slots
// --------------------------------------------------

View File

@ -13,49 +13,52 @@
* @prop --border-width: Width of the item border
* @prop --box-shadow: Box shadow of the item
* @prop --color: Color of the item
*
* @prop --detail-icon-color: Color of the item detail icon
*
* @prop --inner-border-width: Width of the item inner border
* @prop --inner-box-shadow: Box shadow of the item inner
* @prop --inner-padding-bottom: Bottom padding of the item inner
* @prop --inner-padding-end: End padding of the item inner
* @prop --inner-padding-start: Start padding of the item inner
* @prop --inner-padding-top: Top padding of the item inner
*
* @prop --min-height: Minimum height of the item
* @prop --padding-bottom: Bottom padding of the item
* @prop --padding-end: End padding of the item
* @prop --padding-start: Start padding of the item
* @prop --padding-top: Top padding of the item
* @prop --transition: Transition of the item
*
* @prop --highlight-height: The height of the highlight on the item
* @prop --highlight-color-focused: The color of the highlight on the item when focused
* @prop --highlight-color-valid: The color of the highlight on the item when valid
* @prop --highlight-color-invalid: The color of the highlight on the item when invalid
*/
--min-height: #{$item-min-height};
--background: #{current-color(base)};
--background-activated: #{current-color(tint)};
--color: #{current-color(contrast)};
--detail-icon-color: #{current-color(shade)};
--border-radius: 0px;
--border-width: 0px;
--border-style: solid;
--border-color: #{current-color(shade)};
--inner-border-width: 0px;
--padding-top: 0px;
--padding-bottom: 0px;
--padding-end: 0px;
--padding-start: 0px;
--box-shadow: none;
--inner-border-width: 0px;
--inner-padding-top: 0px;
--inner-padding-bottom: 0px;
--inner-padding-start: 0px;
--inner-padding-end: 0px;
--box-shadow: none;
--inner-box-shadow: none;
--highlight-color-focus: #{ion-color(primary, base)};
--highlight-color-valid: #{ion-color(success, base)};
--highlight-color-invalid: #{ion-color(danger, base)};
--highlight-height: 2px;
--show-full-highlight: 0;
--show-inset-highlight: 0;
@include font-smoothing();
display: block;
position: relative;
color: var(--color);
font-family: $font-family-base;
@ -66,10 +69,40 @@
box-sizing: border-box;
}
// Item with Color
// --------------------------------------------------
:host(.ion-color) .item-native {
background: current-color(base);
color: current-color(contrast);
}
:host(.ion-color) .item-native,
:host(.ion-color) .item-inner {
border-color: current-color(shade);
}
:host(.ion-color) .item-detail-icon {
color: current-color(shade);
}
// Activated Item
// --------------------------------------------------
:host(.activated) .item-native {
background: var(--background-activated);
}
:host(.ion-color.activated) .item-native {
background: current-color(tint);
}
// Disabled Item
// --------------------------------------------------
:host(.item-disabled) {
cursor: default;
opacity: .3;
@ -77,15 +110,18 @@
}
// Native Item
// --------------------------------------------------
.item-native {
@include border-radius(var(--border-radius));
@include margin(0);
@include padding(
var(--padding-top),
var(--padding-end),
var(--padding-bottom),
calc(var(--padding-start) + var(--ion-safe-area-left, 0px))
);
@include border-radius(var(--border-radius));
@include margin(0);
@include text-inherit();
display: flex;
@ -119,13 +155,9 @@ button, a {
-webkit-user-drag: none;
}
.item-state {
@include position(null, 0, 0, 0);
position: absolute;
height: var(--highlight-height);
}
// Inner Item
// --------------------------------------------------
.item-inner {
@include margin(0);
@ -138,6 +170,8 @@ button, a {
display: flex;
position: relative;
flex: 1;
flex-direction: inherit;
align-items: inherit;
@ -154,24 +188,9 @@ button, a {
box-sizing: border-box;
}
.input-wrapper {
display: flex;
flex: 1;
flex-direction: inherit;
align-items: inherit;
align-self: stretch;
text-overflow: ellipsis;
overflow: hidden;
box-sizing: border-box;
}
:host([vertical-align-top]),
:host(.item-input) {
align-items: flex-start;
}
// Item Slots
// -----------------------------------------
::slotted(ion-icon) {
font-size: 1.6em;
@ -190,6 +209,25 @@ button, a {
// Item Input
// -----------------------------------------
:host([vertical-align-top]),
:host(.item-input) {
align-items: flex-start;
}
.input-wrapper {
display: flex;
flex: 1;
flex-direction: inherit;
align-items: inherit;
align-self: stretch;
text-overflow: ellipsis;
overflow: hidden;
box-sizing: border-box;
}
:host(.item-label-stacked) .input-wrapper,
:host(.item-label-floating) .input-wrapper {
flex: 1;
@ -202,6 +240,55 @@ button, a {
// pointer-events: auto;
// }
// Item Input Highlight
// --------------------------------------------------
.item-highlight,
.item-inner-highlight {
@include position(null, 0, 0, 0);
position: absolute;
background: var(--highlight-background);
}
.item-highlight {
height: var(--full-highlight-height);
}
.item-inner-highlight {
height: var(--inset-highlight-height);
}
// Item Input Focused
// --------------------------------------------------
:host(.item-interactive.item-has-focus) {
--highlight-background: var(--highlight-color-focused);
// If the item has a full border and highlight is enabled, show the full item highlight
--full-highlight-height: #{calc(var(--highlight-height) * var(--show-full-highlight))};
// If the item has an inset border and highlight is enabled, show the inset item highlight
--inset-highlight-height: #{calc(var(--highlight-height) * var(--show-inset-highlight))};
}
// Item Input Valid
// --------------------------------------------------
:host(.item-interactive.ion-valid) {
--highlight-background: var(--highlight-color-valid);
}
// Item Input Invalid
// --------------------------------------------------
:host(.item-interactive.ion-invalid) {
--highlight-background: var(--highlight-color-invalid);
}
// Item Select
// -----------------------------------------

View File

@ -72,9 +72,6 @@ export class Item implements ComponentInterface {
*/
@Prop() routerDirection?: RouterDirection;
// TODO document this
@Prop() state?: 'valid' | 'invalid' | 'focus';
/**
* The type of the button. Only used when an `onclick` or `button` property is present.
* Possible values are: `"submit"`, `"reset"` and `"button"`.
@ -146,14 +143,14 @@ export class Item implements ComponentInterface {
}
render() {
const { href, detail, mode, win, state, detailIcon, routerDirection, type } = this;
const { href, detail, mode, win, detailIcon, routerDirection, type } = this;
const clickable = this.isClickable();
const TagType = clickable ? (href === undefined ? 'button' : 'a') : 'div';
const attrs = TagType === 'button' ? { type } : { href };
const showDetail = detail !== undefined ? detail : mode === 'ios' && clickable;
return (
return [
<TagType
{...attrs}
class="item-native"
@ -166,10 +163,11 @@ export class Item implements ComponentInterface {
</div>
<slot name="end"></slot>
{showDetail && <ion-icon icon={detailIcon} lazy={false} class="item-detail-icon"></ion-icon>}
<div class="item-inner-highlight"></div>
</div>
{state && <div class="item-state"></div>}
{clickable && mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</TagType>
);
</TagType>,
<div class="item-highlight"></div>
];
}
}

View File

@ -40,6 +40,17 @@ The below chart details the item slots and where it will place the element insid
Items left align text and add an ellipsis when the text is wider than the item. See the [Utility Attributes Documentation](/docs/layout/css-utilities) for attributes that can be added to `<ion-item>` to transform the text.
## Input Highlight
### Highlight Height
Items containing an input will highlight the input with a different color border when focused, valid, or invalid. By default, `md` items have a highlight with a height set to `2px` and `ios` has no highlight (technically the height is set to `0`). The height can be changed using the `--highlight-height` CSS property. To turn off the highlight, set this variable to `0`. For more information on setting CSS properties, see the [theming documentation](/docs/theming/css-variables).
### Highlight Color
The highlight color changes based on the item state, but all of the states use Ionic colors by default. When focused, the input highlight will use the `primary` color. If the input is valid it will use the `success` color, and invalid inputs will use the `danger` color. See the [CSS Custom Properties](#css-custom-properties) section below for the highlight color variables.
<!-- Auto Generated Below -->
@ -56,35 +67,38 @@ Items left align text and add an ellipsis when the text is wider than the item.
| `lines` | `lines` | How the bottom border should be displayed on the item. Available options: `"full"`, `"inset"`, `"none"`. | `"full"`, `"inset"`, `"none"` |
| `mode` | `mode` | The mode determines which platform styles to use. Possible values are: `"ios"` or `"md"`. | `Mode` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `RouterDirection` |
| `state` | `state` | | `"valid"`, `"invalid"`, `"focus"` |
| `type` | `type` | The type of the button. Only used when an `onclick` or `button` property is present. Possible values are: `"submit"`, `"reset"` and `"button"`. Default value is: `"button"` | `"submit"`, `"reset"`, `"button"` |
## CSS Custom Properties
| Name | Description |
| ------------------------ | -------------------------------- |
| `--background` | Background of the item |
| `--background-activated` | Background of the activated item |
| `--border-color` | Color of the item border |
| `--border-radius` | Radius of the item border |
| `--border-style` | Style of the item border |
| `--border-width` | Width of the item border |
| `--box-shadow` | Box shadow of the item |
| `--color` | Color of the item |
| `--detail-icon-color` | Color of the item detail icon |
| `--inner-border-width` | Width of the item inner border |
| `--inner-box-shadow` | Box shadow of the item inner |
| `--inner-padding-bottom` | Bottom padding of the item inner |
| `--inner-padding-end` | End padding of the item inner |
| `--inner-padding-start` | Start padding of the item inner |
| `--inner-padding-top` | Top padding of the item inner |
| `--min-height` | Minimum height of the item |
| `--padding-bottom` | Bottom padding of the item |
| `--padding-end` | End padding of the item |
| `--padding-start` | Start padding of the item |
| `--padding-top` | Top padding of the item |
| `--transition` | Transition of the item |
| Name | Description |
| --------------------------- | --------------------------------------------------- |
| `--background` | Background of the item |
| `--background-activated` | Background of the activated item |
| `--border-color` | Color of the item border |
| `--border-radius` | Radius of the item border |
| `--border-style` | Style of the item border |
| `--border-width` | Width of the item border |
| `--box-shadow` | Box shadow of the item |
| `--color` | Color of the item |
| `--detail-icon-color` | Color of the item detail icon |
| `--highlight-color-focused` | The color of the highlight on the item when focused |
| `--highlight-color-invalid` | The color of the highlight on the item when invalid |
| `--highlight-color-valid` | The color of the highlight on the item when valid |
| `--highlight-height` | The height of the highlight on the item |
| `--inner-border-width` | Width of the item inner border |
| `--inner-box-shadow` | Box shadow of the item inner |
| `--inner-padding-bottom` | Bottom padding of the item inner |
| `--inner-padding-end` | End padding of the item inner |
| `--inner-padding-start` | Start padding of the item inner |
| `--inner-padding-top` | Top padding of the item inner |
| `--min-height` | Minimum height of the item |
| `--padding-bottom` | Bottom padding of the item |
| `--padding-end` | End padding of the item |
| `--padding-start` | Start padding of the item |
| `--padding-top` | Top padding of the item |
| `--transition` | Transition of the item |
----------------------------------------------