From 171020e9d200ccfdef0f01c427b295bb50dd1fef Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Mon, 14 Mar 2022 16:38:37 -0400 Subject: [PATCH] feat(item): counter formatter to customize counter text display (#24336) Resolves #24327 --- angular/src/directives/proxies.ts | 4 +- core/api.txt | 1 + core/src/components.d.ts | 9 + core/src/components/item/item-interface.ts | 2 + core/src/components/item/item.tsx | 37 +++- core/src/components/item/readme.md | 189 ++++++++++++++++-- core/src/components/item/test/counter/e2e.ts | 118 +++++++++-- .../components/item/test/counter/index.html | 16 +- core/src/components/item/usage/angular.md | 33 +++ core/src/components/item/usage/javascript.md | 24 +++ core/src/components/item/usage/react.md | 12 ++ core/src/components/item/usage/stencil.md | 42 ++++ core/src/components/item/usage/vue.md | 35 ++++ core/src/interface.d.ts | 21 +- core/stencil.config.ts | 3 +- packages/vue/src/proxies.ts | 3 +- 16 files changed, 497 insertions(+), 52 deletions(-) create mode 100644 core/src/components/item/item-interface.ts diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index ac79be5095..a323fedfb7 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -834,13 +834,13 @@ export declare interface IonItem extends Components.IonItem {} @ProxyCmp({ defineCustomElementFn: undefined, - inputs: ['button', 'color', 'counter', 'detail', 'detailIcon', 'disabled', 'download', 'fill', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'type'] + inputs: ['button', 'color', 'counter', 'counterFormatter', 'detail', 'detailIcon', 'disabled', 'download', 'fill', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'type'] }) @Component({ selector: 'ion-item', changeDetection: ChangeDetectionStrategy.OnPush, template: '', - inputs: ['button', 'color', 'counter', 'detail', 'detailIcon', 'disabled', 'download', 'fill', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'type'] + inputs: ['button', 'color', 'counter', 'counterFormatter', 'detail', 'detailIcon', 'disabled', 'download', 'fill', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'type'] }) export class IonItem { protected el: HTMLElement; diff --git a/core/api.txt b/core/api.txt index b74dbaa806..f5a0b89220 100644 --- a/core/api.txt +++ b/core/api.txt @@ -557,6 +557,7 @@ ion-item,shadow ion-item,prop,button,boolean,false,false,false ion-item,prop,color,string | undefined,undefined,false,true ion-item,prop,counter,boolean,false,false,false +ion-item,prop,counterFormatter,((inputLength: number, maxLength: number) => string) | undefined,undefined,false,false ion-item,prop,detail,boolean | undefined,undefined,false,false ion-item,prop,detailIcon,string,chevronForward,false,false ion-item,prop,disabled,boolean,false,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 94e417edaa..3f8d3b536d 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -8,6 +8,7 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface"; import { IonicSafeString } from "./utils/sanitization"; import { AlertAttributes } from "./components/alert/alert-interface"; +import { CounterFormatter } from "./components/item/item-interface"; import { PickerColumnItem } from "./components/picker-column-internal/picker-column-internal-interfaces"; import { PickerInternalChangeEventDetail } from "./components/picker-internal/picker-internal-interfaces"; import { PinFormatter } from "./components/range/range-interface"; @@ -1138,6 +1139,10 @@ export namespace Components { * If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. */ "counter": boolean; + /** + * A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength". + */ + "counterFormatter"?: CounterFormatter; /** * If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. */ @@ -4872,6 +4877,10 @@ declare namespace LocalJSX { * If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. */ "counter"?: boolean; + /** + * A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength". + */ + "counterFormatter"?: CounterFormatter; /** * If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. */ diff --git a/core/src/components/item/item-interface.ts b/core/src/components/item/item-interface.ts new file mode 100644 index 0000000000..22cef4c082 --- /dev/null +++ b/core/src/components/item/item-interface.ts @@ -0,0 +1,2 @@ + +export type CounterFormatter = (inputLength: number, maxLength: number) => string; diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx index 7254f9be2c..cc9766ff57 100644 --- a/core/src/components/item/item.tsx +++ b/core/src/components/item/item.tsx @@ -1,4 +1,5 @@ -import { Component, ComponentInterface, Element, Host, Listen, Prop, State, forceUpdate, h } from '@stencil/core'; +import { Component, ComponentInterface, Element, Host, Listen, Prop, State, Watch, forceUpdate, h } from '@stencil/core'; +import { printIonError } from '@utils/logging'; import { chevronForward } from 'ionicons/icons'; import { getIonMode } from '../../global/ionic-global'; @@ -8,6 +9,8 @@ import { raf } from '../../utils/helpers'; import { createColorClasses, hostContext, openURL } from '../../utils/theme'; import { InputChangeEventDetail } from '../input/input-interface'; +import { CounterFormatter } from './item-interface'; + /** * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. * @@ -134,8 +137,19 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac */ @Prop() type: 'submit' | 'reset' | 'button' = 'button'; + /** + * A callback used to format the counter text. + * By default the counter text is set to "itemLength / maxLength". + */ + @Prop() counterFormatter?: CounterFormatter; + @State() counterString: string | null | undefined; + @Watch('counterFormatter') + counterFormatterChanged() { + this.updateCounterOutput(this.getFirstInput()); + } + @Listen('ionChange') handleIonChange(ev: CustomEvent) { if (this.counter && ev.target === this.getFirstInput()) { @@ -296,12 +310,27 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac } private updateCounterOutput(inputEl: HTMLIonInputElement | HTMLIonTextareaElement) { - if (this.counter && !this.multipleInputs && inputEl?.maxlength !== undefined) { - const length = inputEl?.value?.toString().length ?? '0'; - this.counterString = `${length} / ${inputEl.maxlength}`; + const { counter, counterFormatter, defaultCounterFormatter } = this; + if (counter && !this.multipleInputs && inputEl?.maxlength !== undefined) { + const length = inputEl?.value?.toString().length ?? 0; + if (counterFormatter === undefined) { + this.counterString = defaultCounterFormatter(length, inputEl.maxlength); + } else { + try { + this.counterString = counterFormatter(length, inputEl.maxlength); + } catch (e) { + printIonError('Exception in provided `counterFormatter`.', e); + // Fallback to the default counter formatter when an exception happens + this.counterString = defaultCounterFormatter(length, inputEl.maxlength); + } + } } } + private defaultCounterFormatter(length: number, maxlength: number) { + return `${length} / ${maxlength}`; + } + private hasStartEl() { const startEl = this.el.querySelector('[slot="start"]'); if (startEl !== null) { diff --git a/core/src/components/item/readme.md b/core/src/components/item/readme.md index eb9a91a120..ef1951465d 100644 --- a/core/src/components/item/readme.md +++ b/core/src/components/item/readme.md @@ -53,6 +53,10 @@ Items containing an input will highlight the bottom border of the input with a d 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. +### Counter Formatter + +When using `counter`, the default behavior is to format the value that gets displayed as `itemLength / maxLength`. This behavior can be customized by passing in a formatter function to the `counterFormatter` property. See the [Usage](#usage) section for an example. + @@ -397,6 +401,39 @@ The highlight color changes based on the item state, but all of the states use I ``` +### Item Counter + +```html + + Counter + + +``` + +### Item Counter Formatter + +```html + + Counter + + + +``` + +```typescript + +import { Component } from '@angular/core'; + +@Component({…}) +export class MyComponent { + + counterFormatter(inputLength: number, maxLength: number) { + return `${maxLength - inputLength} characters remaining`; + } +} + +``` + ### Javascript @@ -736,6 +773,30 @@ The highlight color changes based on the item state, but all of the states use I ``` +### Item Counter + +```html + + Counter + + +``` + +### Item Counter Formatter + +```html + + Counter + + + + + +``` + ### React @@ -1058,6 +1119,18 @@ export const ItemExamples: React.FC = () => { Range + + {/*-- Item Counter --*/} + + Counter + + + + {/*-- Item Counter Formatter --*/} + `${maxLength - inputLength} characters remaining`}> + Counter + + ); @@ -1509,6 +1582,48 @@ export class ItemExample { } ``` +### Item Counter + +```tsx +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'item-example', + styleUrl: 'item-example.css' +}) +export class ItemExample { + render() { + return [ + + Counter + + + ]; + } +} +``` + +### Item Counter Formatter + +```tsx +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'item-example', + styleUrl: 'item-example.css' +}) +export class ItemExample { + render() { + return [ + `${maxLength - inputLength} characters remaining`}> + Counter + + + ]; + } +} +``` + ### Vue @@ -1925,29 +2040,65 @@ export default defineComponent({ ``` +### Item Counter + +```html + +``` + +### Item Counter Formatter + +```html + + +``` + ## Properties -| Property | Attribute | Description | Type | Default | -| ----------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ---------------- | -| `button` | `button` | If `true`, a button tag will be rendered and the item will be tappable. | `boolean` | `false` | -| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` | -| `counter` | `counter` | If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. | `boolean` | `false` | -| `detail` | `detail` | If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. | `boolean \| undefined` | `undefined` | -| `detailIcon` | `detail-icon` | The icon to use when `detail` is set to `true`. | `string` | `chevronForward` | -| `disabled` | `disabled` | If `true`, the user cannot interact with the item. | `boolean` | `false` | -| `download` | `download` | This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want). | `string \| undefined` | `undefined` | -| `fill` | `fill` | The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. | `"outline" \| "solid" \| undefined` | `undefined` | -| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` | -| `lines` | `lines` | How the bottom border should be displayed on the item. | `"full" \| "inset" \| "none" \| undefined` | `undefined` | -| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` | -| `rel` | `rel` | Specifies the relationship of the target object to the link object. The value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types). | `string \| undefined` | `undefined` | -| `routerAnimation` | -- | When using a router, it specifies the transition animation when navigating to another page using `href`. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` | -| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` | -| `shape` | `shape` | The shape of the item. If "round" it will have increased border radius. | `"round" \| undefined` | `undefined` | -| `target` | `target` | Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. | `string \| undefined` | `undefined` | -| `type` | `type` | The type of the button. Only used when an `onclick` or `button` property is present. | `"button" \| "reset" \| "submit"` | `'button'` | +| Property | Attribute | Description | Type | Default | +| ------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ---------------- | +| `button` | `button` | If `true`, a button tag will be rendered and the item will be tappable. | `boolean` | `false` | +| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` | +| `counter` | `counter` | If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. | `boolean` | `false` | +| `counterFormatter` | -- | A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength". | `((inputLength: number, maxLength: number) => string) \| undefined` | `undefined` | +| `detail` | `detail` | If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. | `boolean \| undefined` | `undefined` | +| `detailIcon` | `detail-icon` | The icon to use when `detail` is set to `true`. | `string` | `chevronForward` | +| `disabled` | `disabled` | If `true`, the user cannot interact with the item. | `boolean` | `false` | +| `download` | `download` | This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want). | `string \| undefined` | `undefined` | +| `fill` | `fill` | The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. | `"outline" \| "solid" \| undefined` | `undefined` | +| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` | +| `lines` | `lines` | How the bottom border should be displayed on the item. | `"full" \| "inset" \| "none" \| undefined` | `undefined` | +| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` | +| `rel` | `rel` | Specifies the relationship of the target object to the link object. The value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types). | `string \| undefined` | `undefined` | +| `routerAnimation` | -- | When using a router, it specifies the transition animation when navigating to another page using `href`. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` | +| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` | +| `shape` | `shape` | The shape of the item. If "round" it will have increased border radius. | `"round" \| undefined` | `undefined` | +| `target` | `target` | Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. | `string \| undefined` | `undefined` | +| `type` | `type` | The type of the button. Only used when an `onclick` or `button` property is present. | `"button" \| "reset" \| "submit"` | `'button'` | ## Slots diff --git a/core/src/components/item/test/counter/e2e.ts b/core/src/components/item/test/counter/e2e.ts index cfbaf1bc20..40be21c701 100644 --- a/core/src/components/item/test/counter/e2e.ts +++ b/core/src/components/item/test/counter/e2e.ts @@ -1,19 +1,111 @@ -import { newE2EPage } from '@stencil/core/testing'; +import { E2EPage, newE2EPage } from '@stencil/core/testing'; -test('item: counter', async () => { - const page = await newE2EPage({ - url: '/src/components/item/test/counter?ionic:_testing=true' +describe('item: counter', () => { + + it('should match existing visual screenshots', async () => { + const page = await newE2EPage({ + url: '/src/components/item/test/counter?ionic:_testing=true' + }); + + const compare = await page.compareScreenshot(); + expect(compare).toMatchScreenshot(); }); - const compare = await page.compareScreenshot(); - expect(compare).toMatchScreenshot(); -}); + describe('custom formatter', () => { + + let page: E2EPage; + + beforeEach(async () => { + page = await newE2EPage({ + url: '/src/components/item/test/counter?ionic:_testing=true' + }); + }); + + it('should format on load', async () => { + const itemCounter = await page.find('#customFormatter >>> .item-counter'); + + expect(itemCounter.textContent).toBe('20 characters left'); + }); + + it('should format on input', async () => { + const input = await page.find('#customFormatter ion-input'); + + await input.click(); + await input.type('abcde'); + + await page.waitForChanges(); + + const itemCounter = await page.find('#customFormatter >>> .item-counter'); + + expect(itemCounter.textContent).toBe('15 characters left'); + }); + + it('should format after changing the counterFormatter', async () => { + let itemCounter = await page.find('#customFormatter >>> .item-counter'); + + expect(itemCounter.textContent).toBe('20 characters left'); + + await page.$eval('#customFormatter', (el: any) => { + el.counterFormatter = () => { + return 'test label'; + }; + }); + await page.waitForChanges(); + + itemCounter = await page.find('#customFormatter >>> .item-counter'); + + expect(itemCounter.textContent).toBe('test label'); + }); + + describe('when an exception occurs', () => { + + const logs = []; + + beforeEach(async () => { + page = await newE2EPage({ + html: ` + + + ` + }); + + page.on('console', ev => { + if (ev.type() === 'error') { + logs.push(ev.text()); + } + }); + + let itemCounter = await page.find('ion-item >>> .item-counter'); + + expect(itemCounter.textContent).toBe('0 / 20'); + + await page.$eval('ion-item', (el: any) => { + el.counterFormatter = () => { + throw new Error('This is an expected error'); + }; + }); + await page.waitForChanges(); + + }); + + it('should default the formatting to length / maxlength', async () => { + const input = await page.find('ion-input'); + + await input.click(); + await input.type('abcde'); + + const itemCounter = await page.find('ion-item >>> .item-counter'); + + expect(itemCounter.textContent).toBe('5 / 20'); + }); + + it('should log an error', () => { + expect(logs.length).toBeGreaterThan(0); + expect(logs[0]).toMatch('[Ionic Error]: Exception in provided `counterFormatter`.'); + }); + + }); -test('item: counter-rtl', async () => { - const page = await newE2EPage({ - url: '/src/components/item/test/counter?ionic:_testing=true&rtl=true' }); - const compare = await page.compareScreenshot(); - expect(compare).toMatchScreenshot(); -}); +}) diff --git a/core/src/components/item/test/counter/index.html b/core/src/components/item/test/counter/index.html index 841578862b..8510254020 100644 --- a/core/src/components/item/test/counter/index.html +++ b/core/src/components/item/test/counter/index.html @@ -26,17 +26,29 @@ - Counter + Counter - Counter with value + Counter with value + + + Counter with custom formatter + + + + diff --git a/core/src/components/item/usage/angular.md b/core/src/components/item/usage/angular.md index 18851d0dfb..190ada7b09 100644 --- a/core/src/components/item/usage/angular.md +++ b/core/src/components/item/usage/angular.md @@ -333,3 +333,36 @@ ``` + +### Item Counter + +```html + + Counter + + +``` + +### Item Counter Formatter + +```html + + Counter + + + +``` + +```typescript + +import { Component } from '@angular/core'; + +@Component({…}) +export class MyComponent { + + counterFormatter(inputLength: number, maxLength: number) { + return `${maxLength - inputLength} characters remaining`; + } +} + +``` diff --git a/core/src/components/item/usage/javascript.md b/core/src/components/item/usage/javascript.md index a23543adda..0f7df63a96 100644 --- a/core/src/components/item/usage/javascript.md +++ b/core/src/components/item/usage/javascript.md @@ -333,3 +333,27 @@ ``` + +### Item Counter + +```html + + Counter + + +``` + +### Item Counter Formatter + +```html + + Counter + + + + + +``` \ No newline at end of file diff --git a/core/src/components/item/usage/react.md b/core/src/components/item/usage/react.md index d2bfddd70d..6d371571be 100644 --- a/core/src/components/item/usage/react.md +++ b/core/src/components/item/usage/react.md @@ -317,6 +317,18 @@ export const ItemExamples: React.FC = () => { Range + + {/*-- Item Counter --*/} + + Counter + + + + {/*-- Item Counter Formatter --*/} + `${maxLength - inputLength} characters remaining`}> + Counter + + ); diff --git a/core/src/components/item/usage/stencil.md b/core/src/components/item/usage/stencil.md index 13c3ec4bdd..cb295f8a55 100644 --- a/core/src/components/item/usage/stencil.md +++ b/core/src/components/item/usage/stencil.md @@ -438,4 +438,46 @@ export class ItemExample { ]; } } +``` + +### Item Counter + +```tsx +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'item-example', + styleUrl: 'item-example.css' +}) +export class ItemExample { + render() { + return [ + + Counter + + + ]; + } +} +``` + +### Item Counter Formatter + +```tsx +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'item-example', + styleUrl: 'item-example.css' +}) +export class ItemExample { + render() { + return [ + `${maxLength - inputLength} characters remaining`}> + Counter + + + ]; + } +} ``` \ No newline at end of file diff --git a/core/src/components/item/usage/vue.md b/core/src/components/item/usage/vue.md index 7aeb2626a0..1ce5168433 100644 --- a/core/src/components/item/usage/vue.md +++ b/core/src/components/item/usage/vue.md @@ -410,3 +410,38 @@ export default defineComponent({ }); ``` + +### Item Counter + +```html + +``` + +### Item Counter Formatter + +```html + + +``` \ No newline at end of file diff --git a/core/src/interface.d.ts b/core/src/interface.d.ts index 99a59deaac..0b32f21cbc 100644 --- a/core/src/interface.d.ts +++ b/core/src/interface.d.ts @@ -11,6 +11,7 @@ export * from './components/checkbox/checkbox-interface'; export * from './components/datetime/datetime-interface'; export * from './components/infinite-scroll/infinite-scroll-interface'; export * from './components/input/input-interface'; +export * from './components/item/item-interface'; export * from './components/item-sliding/item-sliding-interface'; export * from './components/loading/loading-interface'; export * from './components/menu/menu-interface'; @@ -43,13 +44,13 @@ export { Gesture, GestureConfig, GestureDetail } from './utils/gesture'; // From: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete export type AutocompleteTypes = ( -| 'on' | 'off' | 'name' | 'honorific-prefix' | 'given-name' | 'additional-name' | 'family-name' | 'honorific-suffix' -| 'nickname' | 'email' | 'username' | 'new-password' | 'current-password' | 'one-time-code' | 'organization-title' | 'organization' -| 'street-address' | 'address-line1' | 'address-line2' | 'address-line3' | 'address-level4' | 'address-level3' | 'address-level2' -| 'address-level1' | 'country' | 'country-name' | 'postal-code' | 'cc-name' | 'cc-given-name' | 'cc-additional-name' | 'cc-family-name' -| 'cc-family-name' | 'cc-number' | 'cc-exp' | 'cc-exp-month' | 'cc-exp-year' | 'cc-csc' | 'cc-type' | 'transaction-currency' | 'transaction-amount' -| 'language' | 'bday' | 'bday-day' | 'bday-month' | 'bday-year' | 'sex' | 'tel' | 'tel-country-code' | 'tel-national' | 'tel-area-code' | 'tel-local' -| 'tel-extension' | 'impp' | 'url' | 'photo'); + | 'on' | 'off' | 'name' | 'honorific-prefix' | 'given-name' | 'additional-name' | 'family-name' | 'honorific-suffix' + | 'nickname' | 'email' | 'username' | 'new-password' | 'current-password' | 'one-time-code' | 'organization-title' | 'organization' + | 'street-address' | 'address-line1' | 'address-line2' | 'address-line3' | 'address-level4' | 'address-level3' | 'address-level2' + | 'address-level1' | 'country' | 'country-name' | 'postal-code' | 'cc-name' | 'cc-given-name' | 'cc-additional-name' | 'cc-family-name' + | 'cc-family-name' | 'cc-number' | 'cc-exp' | 'cc-exp-month' | 'cc-exp-year' | 'cc-csc' | 'cc-type' | 'transaction-currency' | 'transaction-amount' + | 'language' | 'bday' | 'bday-day' | 'bday-month' | 'bday-year' | 'sex' | 'tel' | 'tel-country-code' | 'tel-national' | 'tel-area-code' | 'tel-local' + | 'tel-extension' | 'impp' | 'url' | 'photo'); export type TextFieldTypes = 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | 'time' | 'week' | 'month' | 'datetime-local'; @@ -59,7 +60,7 @@ export type Color = PredefinedColors | string; export type Mode = "ios" | "md"; export type ComponentTags = string; export type ComponentRef = Function | HTMLElement | string | null; -export type ComponentProps = {[key: string]: any}; +export type ComponentProps = { [key: string]: any }; export type CssClassMap = { [className: string]: boolean }; export type BackButtonEvent = CustomEvent; @@ -84,12 +85,12 @@ export { NavComponentWithProps } from "./components/nav/nav-interface"; declare module "./components" { export namespace Components { - export interface IonIcon extends IoniconsComponents.IonIcon{} + export interface IonIcon extends IoniconsComponents.IonIcon { } } } declare module "./components" { export namespace JSX { - export interface IonIcon extends IoniconsJSX.IonIcon {} + export interface IonIcon extends IoniconsJSX.IonIcon { } } } diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 5b585b5e4c..af5814d55f 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -262,7 +262,8 @@ export const config: Config = { pixelmatchThreshold: 0.05, waitBeforeScreenshot: 20, moduleNameMapper: { - "@utils/test": ["/src/utils/test/utils"] + "@utils/test": ["/src/utils/test/utils"], + "@utils/logging": ["/src/utils/logging"] }, emulate: [ { diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 11a64ee68c..4964a67998 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -429,7 +429,8 @@ export const IonItem = /*@__PURE__*/ defineContainer('ion-item', de 'routerAnimation', 'routerDirection', 'target', - 'type' + 'type', + 'counterFormatter' ]);