UNPKG

@ionic/core

Version:
508 lines (507 loc) • 22.5 kB
/*! * (C) Ionic http://ionicframework.com - MIT License */ import { Host, forceUpdate, h } from "@stencil/core"; import { inheritAttributes, raf } from "../../utils/helpers"; import { createColorClasses, hostContext, openURL } from "../../utils/theme"; import { chevronForward } from "ionicons/icons"; import { getIonMode } from "../../global/ionic-global"; /** * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. * * @slot - Content is placed between the named slots if provided without a slot. * @slot start - Content is placed to the left of the item text in LTR, and to the right in RTL. * @slot end - Content is placed to the right of the item text in LTR, and to the left in RTL. * * @part native - The native HTML button, anchor or div element that wraps all child elements. * @part detail-icon - The chevron icon for the item. Only applies when `detail="true"`. */ export class Item { constructor() { this.labelColorStyles = {}; this.itemStyles = new Map(); this.inheritedAriaAttributes = {}; this.multipleInputs = false; this.focusable = true; this.color = undefined; this.button = false; this.detail = undefined; this.detailIcon = chevronForward; this.disabled = false; this.download = undefined; this.href = undefined; this.rel = undefined; this.lines = undefined; this.routerAnimation = undefined; this.routerDirection = 'forward'; this.target = undefined; this.type = 'button'; } buttonChanged() { // Update the focusable option when the button option is changed this.focusable = this.isFocusable(); } labelColorChanged(ev) { const { color } = this; // There will be a conflict with item color if // we apply the label color to item, so we ignore // the label color if the user sets a color on item if (color === undefined) { this.labelColorStyles = ev.detail; } } itemStyle(ev) { ev.stopPropagation(); const tagName = ev.target.tagName; const updatedStyles = ev.detail; const newStyles = {}; const childStyles = this.itemStyles.get(tagName) || {}; let hasStyleChange = false; Object.keys(updatedStyles).forEach((key) => { if (updatedStyles[key]) { const itemKey = `item-${key}`; if (!childStyles[itemKey]) { hasStyleChange = true; } newStyles[itemKey] = true; } }); if (!hasStyleChange && Object.keys(newStyles).length !== Object.keys(childStyles).length) { hasStyleChange = true; } if (hasStyleChange) { this.itemStyles.set(tagName, newStyles); forceUpdate(this); } } connectedCallback() { this.hasStartEl(); } componentWillLoad() { this.inheritedAriaAttributes = inheritAttributes(this.el, ['aria-label']); } componentDidLoad() { raf(() => { this.setMultipleInputs(); this.focusable = this.isFocusable(); }); } // If the item contains multiple clickable elements and/or inputs, then the item // should not have a clickable input cover over the entire item to prevent // interfering with their individual click events setMultipleInputs() { // The following elements have a clickable cover that is relative to the entire item const covers = this.el.querySelectorAll('ion-checkbox, ion-datetime, ion-select, ion-radio'); // The following elements can accept focus alongside the previous elements // therefore if these elements are also a child of item, we don't want the // input cover on top of those interfering with their clicks const inputs = this.el.querySelectorAll('ion-input, ion-range, ion-searchbar, ion-segment, ion-textarea, ion-toggle'); // The following elements should also stay clickable when an input with cover is present const clickables = this.el.querySelectorAll('ion-router-link, ion-button, a, button'); // Check for multiple inputs to change the position of the input cover to relative // for all of the covered inputs above this.multipleInputs = covers.length + inputs.length > 1 || covers.length + clickables.length > 1 || (covers.length > 0 && this.isClickable()); } // If the item contains an input including a checkbox, datetime, select, or radio // then the item will have a clickable input cover that covers the item // that should get the hover, focused and activated states UNLESS it has multiple // inputs, then those need to individually get each click hasCover() { const inputs = this.el.querySelectorAll('ion-checkbox, ion-datetime, ion-select, ion-radio'); return inputs.length === 1 && !this.multipleInputs; } // If the item has an href or button property it will render a native // anchor or button that is clickable isClickable() { return this.href !== undefined || this.button; } canActivate() { return this.isClickable() || this.hasCover(); } isFocusable() { const focusableChild = this.el.querySelector('.ion-focusable'); return this.canActivate() || focusableChild !== null; } hasStartEl() { const startEl = this.el.querySelector('[slot="start"]'); if (startEl !== null) { this.el.classList.add('item-has-start-slot'); } } getFirstInteractive() { const controls = this.el.querySelectorAll('ion-toggle:not([disabled]), ion-checkbox:not([disabled]), ion-radio:not([disabled]), ion-select:not([disabled]), ion-input:not([disabled]), ion-textarea:not([disabled])'); return controls[0]; } render() { const { detail, detailIcon, download, labelColorStyles, lines, disabled, href, rel, target, routerAnimation, routerDirection, inheritedAriaAttributes, multipleInputs, } = this; const childStyles = {}; const mode = getIonMode(this); const clickable = this.isClickable(); const canActivate = this.canActivate(); const TagType = clickable ? (href === undefined ? 'button' : 'a') : 'div'; const attrs = TagType === 'button' ? { type: this.type } : { download, href, rel, target, }; let clickFn = {}; const firstInteractive = this.getFirstInteractive(); // Only set onClick if the item is clickable to prevent screen // readers from reading all items as clickable if (clickable || (firstInteractive !== undefined && !multipleInputs)) { clickFn = { onClick: (ev) => { if (clickable) { openURL(href, ev, routerDirection, routerAnimation); } if (firstInteractive !== undefined && !multipleInputs) { const path = ev.composedPath(); const target = path[0]; if (ev.isTrusted) { /** * Dispatches a click event to the first interactive element, * when it is the result of a user clicking on the item. * * We check if the click target is in the shadow root, * which means the user clicked on the .item-native or * .item-inner padding. */ const clickedWithinShadowRoot = this.el.shadowRoot.contains(target); if (clickedWithinShadowRoot) { /** * For input/textarea clicking the padding should focus the * text field (thus making it editable). For everything else, * we want to click the control so it activates. */ if (firstInteractive.tagName === 'ION-INPUT' || firstInteractive.tagName === 'ION-TEXTAREA') { firstInteractive.setFocus(); } else { firstInteractive.click(); } } } } }, }; } const showDetail = detail !== undefined ? detail : mode === 'ios' && clickable; this.itemStyles.forEach((value) => { Object.assign(childStyles, value); }); const ariaDisabled = disabled || childStyles['item-interactive-disabled'] ? 'true' : null; const inList = hostContext('ion-list', this.el) && !hostContext('ion-radio-group', this.el); /** * Inputs and textareas do not need to show a cursor pointer. * However, other form controls such as checkboxes and radios do. */ const firstInteractiveNeedsPointerCursor = firstInteractive !== undefined && !['ION-INPUT', 'ION-TEXTAREA'].includes(firstInteractive.tagName); return (h(Host, { key: '02f540d8666146b5f2665ba6b39dfe774c65d5e6', "aria-disabled": ariaDisabled, class: Object.assign(Object.assign(Object.assign({}, childStyles), labelColorStyles), createColorClasses(this.color, { item: true, [mode]: true, 'item-lines-default': lines === undefined, [`item-lines-${lines}`]: lines !== undefined, 'item-control-needs-pointer-cursor': firstInteractiveNeedsPointerCursor, 'item-disabled': disabled, 'in-list': inList, 'item-multiple-inputs': this.multipleInputs, 'ion-activatable': canActivate, 'ion-focusable': this.focusable, 'item-rtl': document.dir === 'rtl', })), role: inList ? 'listitem' : null }, h(TagType, Object.assign({ key: '91cf3eedc2d7eca40a45fc3b0aaae5a87d387eb7' }, attrs, inheritedAriaAttributes, { class: "item-native", part: "native", disabled: disabled }, clickFn), h("slot", { key: 'ae4c679ca903a78a7c933c1d02f21f5d677b6388', name: "start" }), h("div", { key: 'a61de72dce135b6dce18e6fa19680524958924dd', class: "item-inner" }, h("div", { key: 'd5b09966b6de8d6b4da8e139875edec4eeb1ae27', class: "input-wrapper" }, h("slot", { key: '79077e5708a1783b135fab6f8caff9fb2f6f387a' })), h("slot", { key: 'cdcc71689a87e985d32170ab98538ef25227f384', name: "end" }), showDetail && (h("ion-icon", { key: '1e82f2d0e2d2fc18bd3429a7c72e4e57447cad7a', icon: detailIcon, lazy: false, class: "item-detail-icon", part: "detail-icon", "aria-hidden": "true", "flip-rtl": detailIcon === chevronForward }))), canActivate && mode === 'md' && h("ion-ripple-effect", { key: 'ab9842d0d9c64408573f07323e10f9ed29fbe6e2' })))); } static get is() { return "ion-item"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "ios": ["item.ios.scss"], "md": ["item.md.scss"] }; } static get styleUrls() { return { "ios": ["item.ios.css"], "md": ["item.md.css"] }; } static get properties() { return { "color": { "type": "string", "mutable": false, "complexType": { "original": "Color", "resolved": "\"danger\" | \"dark\" | \"light\" | \"medium\" | \"primary\" | \"secondary\" | \"success\" | \"tertiary\" | \"warning\" | string & Record<never, never> | undefined", "references": { "Color": { "location": "import", "path": "../../interface", "id": "src/interface.d.ts::Color" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "The color to use from your application's color palette.\nDefault options are: `\"primary\"`, `\"secondary\"`, `\"tertiary\"`, `\"success\"`, `\"warning\"`, `\"danger\"`, `\"light\"`, `\"medium\"`, and `\"dark\"`.\nFor more information on colors, see [theming](/docs/theming/basics)." }, "attribute": "color", "reflect": true }, "button": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "If `true`, a button tag will be rendered and the item will be tappable." }, "attribute": "button", "reflect": false, "defaultValue": "false" }, "detail": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode`\nis `ios` and an `href` or `button` property is present." }, "attribute": "detail", "reflect": false }, "detailIcon": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The icon to use when `detail` is set to `true`." }, "attribute": "detail-icon", "reflect": false, "defaultValue": "chevronForward" }, "disabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "If `true`, the user cannot interact with the item." }, "attribute": "disabled", "reflect": true, "defaultValue": "false" }, "download": { "type": "string", "mutable": false, "complexType": { "original": "string | undefined", "resolved": "string | undefined", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "This attribute instructs browsers to download a URL instead of navigating to\nit, so the user will be prompted to save it as a local file. If the attribute\nhas a value, it is used as the pre-filled file name in the Save prompt\n(the user can still change the file name if they want)." }, "attribute": "download", "reflect": false }, "href": { "type": "string", "mutable": false, "complexType": { "original": "string | undefined", "resolved": "string | undefined", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Contains a URL or a URL fragment that the hyperlink points to.\nIf this property is set, an anchor tag will be rendered." }, "attribute": "href", "reflect": false }, "rel": { "type": "string", "mutable": false, "complexType": { "original": "string | undefined", "resolved": "string | undefined", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the relationship of the target object to the link object.\nThe value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types)." }, "attribute": "rel", "reflect": false }, "lines": { "type": "string", "mutable": false, "complexType": { "original": "'full' | 'inset' | 'none'", "resolved": "\"full\" | \"inset\" | \"none\" | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "How the bottom border should be displayed on the item." }, "attribute": "lines", "reflect": false }, "routerAnimation": { "type": "unknown", "mutable": false, "complexType": { "original": "AnimationBuilder | undefined", "resolved": "((baseEl: any, opts?: any) => Animation) | undefined", "references": { "AnimationBuilder": { "location": "import", "path": "../../interface", "id": "src/interface.d.ts::AnimationBuilder" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "When using a router, it specifies the transition animation when navigating to\nanother page using `href`." } }, "routerDirection": { "type": "string", "mutable": false, "complexType": { "original": "RouterDirection", "resolved": "\"back\" | \"forward\" | \"root\"", "references": { "RouterDirection": { "location": "import", "path": "../router/utils/interface", "id": "src/components/router/utils/interface.ts::RouterDirection" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "When using a router, it specifies the transition direction when navigating to\nanother page using `href`." }, "attribute": "router-direction", "reflect": false, "defaultValue": "'forward'" }, "target": { "type": "string", "mutable": false, "complexType": { "original": "string | undefined", "resolved": "string | undefined", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies where to display the linked URL.\nOnly applies when an `href` is provided.\nSpecial keywords: `\"_blank\"`, `\"_self\"`, `\"_parent\"`, `\"_top\"`." }, "attribute": "target", "reflect": false }, "type": { "type": "string", "mutable": false, "complexType": { "original": "'submit' | 'reset' | 'button'", "resolved": "\"button\" | \"reset\" | \"submit\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The type of the button. Only used when an `onclick` or `button` property is present." }, "attribute": "type", "reflect": false, "defaultValue": "'button'" } }; } static get states() { return { "multipleInputs": {}, "focusable": {} }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "button", "methodName": "buttonChanged" }]; } static get listeners() { return [{ "name": "ionColor", "method": "labelColorChanged", "target": undefined, "capture": false, "passive": false }, { "name": "ionStyle", "method": "itemStyle", "target": undefined, "capture": false, "passive": false }]; } }