Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
621dc13
feat(input): add new js and css props
liamdebeasi Oct 31, 2022
d24428c
chore(): copy updates
liamdebeasi Oct 31, 2022
0402ae8
test(input): move old tests to legacy directory
liamdebeasi Oct 31, 2022
7643d2d
feat(input): add legacy vs modern template rendering
liamdebeasi Oct 31, 2022
259a3a2
refactor(form): add form controller
liamdebeasi Oct 31, 2022
a989e94
chore(): update comments
liamdebeasi Oct 31, 2022
2a80a5f
chore(): update tests
liamdebeasi Oct 31, 2022
08ea3a0
chore(): add moved screenshots
liamdebeasi Oct 31, 2022
bd80054
chore(): add moved screenshots
liamdebeasi Oct 31, 2022
19f4994
chore(): fix test file
liamdebeasi Oct 31, 2022
d02c144
chore(): prettier
liamdebeasi Oct 31, 2022
cea5615
feat(input): add base functionality and tests
liamdebeasi Oct 31, 2022
8d7f0c4
feat(input): add missing fixed label
liamdebeasi Oct 31, 2022
6eabc11
feat(input): add basic help text rendering
liamdebeasi Oct 31, 2022
0259004
feat(input): add helper and error text
liamdebeasi Oct 31, 2022
5f765a0
chore(): update descriptions for clarity
liamdebeasi Oct 31, 2022
3e4f70c
test(input): make sure correct content is rendered
liamdebeasi Oct 31, 2022
232d868
test(input): make sure error color can be customized
liamdebeasi Oct 31, 2022
2f6019b
chore(): update test
liamdebeasi Oct 31, 2022
5c3bb0c
chore(): add updated snapshots
Ionitron Oct 31, 2022
f8dc97f
refactor(item): deprecate helper and error slots
liamdebeasi Nov 1, 2022
23af9da
Merge branch '2591-helper' of https://github.com/ionic-team/ionic-fra…
liamdebeasi Nov 1, 2022
eeb7124
feat(input): add counter support
liamdebeasi Nov 1, 2022
b9da66c
refactor(item): deprecate counter and counterFormatter
liamdebeasi Nov 1, 2022
516a615
chore(): a few tweaks
liamdebeasi Nov 1, 2022
2d07367
test(input): clean up tests
liamdebeasi Nov 1, 2022
4a4aca7
Merge remote-tracking branch 'origin/2591-helper' into 2591-counter
liamdebeasi Nov 1, 2022
f855134
test(input): add counter tests
liamdebeasi Nov 1, 2022
a7e4426
chore(): update prop description
liamdebeasi Nov 1, 2022
fff8ed1
Merge remote-tracking branch 'origin/FW-2591' into 2591-props
liamdebeasi Nov 1, 2022
563f177
Update core/src/components/input/input.tsx
liamdebeasi Nov 1, 2022
e1696af
chore(): run build
liamdebeasi Nov 1, 2022
a17a9ae
Merge remote-tracking branch 'origin/2591-props' into 2591-template
liamdebeasi Nov 1, 2022
b458fe7
Merge remote-tracking branch 'origin/2591-template' into 2591-base
liamdebeasi Nov 1, 2022
eace2ec
Merge branch '2591-base' into 2591-helper
liamdebeasi Nov 1, 2022
8be3a58
Merge remote-tracking branch 'origin/2591-helper' into 2591-counter
liamdebeasi Nov 1, 2022
f556aee
chore(input): remove justify property
liamdebeasi Nov 1, 2022
f65f289
chore(): add updated snapshots
Ionitron Nov 1, 2022
6b8131a
Update core/src/components/input/input.tsx
liamdebeasi Nov 2, 2022
e0b5cd6
feat(input): add missing fill property
liamdebeasi Nov 2, 2022
5a0f812
refactor(): explicitly add element type
liamdebeasi Nov 2, 2022
2bf7b10
test(input): mask test uses new syntax
liamdebeasi Nov 2, 2022
1cc337a
refactor(input): access props once
liamdebeasi Nov 2, 2022
9f04dd1
Merge branch '2591-base' into 2591-helper
liamdebeasi Nov 2, 2022
4272366
chore(): fix el type
liamdebeasi Nov 2, 2022
6d09ad6
Merge remote-tracking branch 'origin/2591-props' into 2591-template
liamdebeasi Nov 2, 2022
9632c22
Merge remote-tracking branch 'origin/2591-template' into 2591-base
liamdebeasi Nov 2, 2022
79ff704
Merge remote-tracking branch 'origin/2591-base' into 2591-helper
liamdebeasi Nov 2, 2022
aa7edae
Merge remote-tracking branch 'origin/2591-helper' into 2591-counter
liamdebeasi Nov 2, 2022
613bdcf
refactor(input): clean up counter logic
liamdebeasi Nov 2, 2022
d0f8942
chore(): sync
liamdebeasi Nov 7, 2022
4c15d30
chore(): lint
liamdebeasi Nov 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1205,10 +1205,12 @@ export namespace Components {
"color"?: Color;
/**
* 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`.
* @deprecated Use the `counter` property on `ion-input` or `ion-textarea` instead.
*/
"counter": boolean;
/**
* A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength".
* @deprecated Use the `counterFormatter` property on `ion-input` or `ion-textarea` instead.
*/
"counterFormatter"?: CounterFormatter;
/**
Expand Down Expand Up @@ -5055,10 +5057,12 @@ declare namespace LocalJSX {
"color"?: Color;
/**
* 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`.
* @deprecated Use the `counter` property on `ion-input` or `ion-textarea` instead.
*/
"counter"?: boolean;
/**
* A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength".
* @deprecated Use the `counterFormatter` property on `ion-input` or `ion-textarea` instead.
*/
"counterFormatter"?: CounterFormatter;
/**
Expand Down
6 changes: 6 additions & 0 deletions core/src/components/input/input.md.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@

background-size: $input-md-input-clear-icon-size;
}

// Input Max Length Counter
// ----------------------------------------------------------------
.input-bottom .counter {
letter-spacing: .0333333333em;
}
18 changes: 18 additions & 0 deletions core/src/components/input/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,21 @@
:host(.ion-invalid) .input-bottom .helper-text {
display: none;
}

// Input Max Length Counter
// ----------------------------------------------------------------

.input-bottom .counter {
/**
* Counter should always be at
* the end of the container even
* when no helper/error texts are used.
*/
@include margin-horizontal(auto, null);

color: #{$background-color-step-550};

white-space: nowrap;

padding-inline-start: 16px;
}
23 changes: 20 additions & 3 deletions core/src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type { Attributes } from '../../utils/helpers';
import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers';
import { createColorClasses } from '../../utils/theme';

import { getCounterText } from './input.utils';

/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*/
Expand Down Expand Up @@ -521,20 +523,35 @@ export class Input implements ComponentInterface {
return [<div class="helper-text">{helperText}</div>, <div class="error-text">{errorText}</div>];
}

private renderCounter() {
const { counter, maxlength, counterFormatter, value } = this;
if (counter !== true || maxlength === undefined) {
return;
}

return <div class="counter">{getCounterText(value, maxlength, counterFormatter)}</div>;
}

/**
* Responsible for rendering helper text,
* error text, and counter. This element should only
* be rendered if hint text is set or counter is enabled.
*/
private renderBottomContent() {
const { helperText, errorText } = this;
const { counter, helperText, errorText, maxlength } = this;

const hasHintText = helperText !== undefined || errorText !== undefined;
if (!hasHintText) {
const hasCounter = counter === true && maxlength !== undefined;
if (!hasHintText && !hasCounter) {
return;
}

return <div class="input-bottom">{this.renderHintText()}</div>;
return (
<div class="input-bottom">
{this.renderHintText()}
{this.renderCounter()}
</div>
);
}

private renderInput() {
Expand Down
34 changes: 34 additions & 0 deletions core/src/components/input/input.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { printIonError } from '@utils/logging';

export const getCounterText = (
value: string | number | null | undefined,
maxLength: number,
counterFormatter?: (inputLength: number, maxLength: number) => string
) => {
const valueLength = value == null ? 0 : value.toString().length;
const defaultCounterText = defaultCounterFormatter(valueLength, maxLength);

/**
* If developers did not pass a custom formatter,
* use the default one.
*/
if (counterFormatter === undefined) {
return defaultCounterText;
}

/**
* Otherwise, try to use the custom formatter
* and fallback to the default formatter if
* there was an error.
*/
try {
return counterFormatter(valueLength, maxLength);
} catch (e) {
printIonError('Exception in provided `counterFormatter`.', e);
return defaultCounterText;
}
};

const defaultCounterFormatter = (length: number, maxlength: number) => {
return `${length} / ${maxlength}`;
};
33 changes: 33 additions & 0 deletions core/src/components/input/test/bottom-content/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,40 @@ <h2>Custom Error Color</h2>
error-text="Please enter a valid email"
></ion-input>
</div>
<div class="grid-item">
<h2>Counter</h2>
<ion-input label="Email" counter="true" maxlength="100"></ion-input>
</div>

<div class="grid-item">
<h2>Custom Counter</h2>
<ion-input id="custom-counter" label="Email" counter="true" maxlength="100"></ion-input>
</div>

<div class="grid-item">
<h2>Counter with Helper</h2>
<ion-input label="Email" counter="true" maxlength="100" helper-text="Enter an email"></ion-input>
</div>

<div class="grid-item">
<h2>Counter with Error</h2>
<ion-input
class="ion-invalid"
label="Email"
counter="true"
maxlength="100"
error-text="Please enter a valid email"
></ion-input>
</div>
</div>

<script>
const customCounterInput = document.querySelector('ion-input#custom-counter');
customCounterInput.counterFormatter = (inputLength, maxLength) => {
const length = maxLength - inputLength;
return `${maxLength - inputLength} characters left`;
};
</script>
</ion-content>
</ion-app>
</body>
Expand Down
58 changes: 58 additions & 0 deletions core/src/components/input/test/bottom-content/input.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,61 @@ test.describe('input: hint text', () => {
});
});
});
test.describe('input: counter', () => {
test.describe('input: counter functionality', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
skip.mode('ios', 'Rendering is the same across modes');
});
test('should not activate if maxlength is not specified even if bottom content is visible', async ({ page }) => {
await page.setContent(`
<ion-input label="my label" counter="true" helper-text="helper text"></ion-input>
`);
const itemCounter = page.locator('ion-input .counter');
await expect(itemCounter).toBeHidden();
});
test('default formatter should be used', async ({ page }) => {
await page.setContent(`
<ion-input label="my label" counter="true" maxlength="20"></ion-input>
`);
const itemCounter = page.locator('ion-input .counter');
expect(await itemCounter.textContent()).toBe('0 / 20');
});
test('custom formatter should be used when provided', async ({ page }) => {
await page.setContent(`
<ion-input label="my label" counter="true" maxlength="20"></ion-input>

<script>
const input = document.querySelector('ion-input');
input.counterFormatter = (inputLength, maxLength) => {
const length = maxLength - inputLength;
return length.toString() + ' characters left';
};
</script>
`);

const input = page.locator('ion-input input');
const itemCounter = page.locator('ion-input .counter');
expect(await itemCounter.textContent()).toBe('20 characters left');

await input.click();
await input.type('abcde');

await page.waitForChanges();

expect(await itemCounter.textContent()).toBe('15 characters left');
});
});
test.describe('input: counter rendering', () => {
test.describe('regular inputs', () => {
test('should not have visual regressions when rendering counter', async ({ page }) => {
await page.setContent(`<ion-input counter="true" maxlength="20" label="my input"></ion-input>`);

const bottomEl = page.locator('ion-input .input-bottom');
expect(await bottomEl.screenshot()).toMatchSnapshot(
`input-bottom-content-counter-${page.getSnapshotSettings()}.png`
);
});
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 17 additions & 1 deletion core/src/components/item/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac

/**
* 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`.
* @deprecated Use the `counter` property on `ion-input` or `ion-textarea` instead.
*/
@Prop() counter = false;

Expand Down Expand Up @@ -141,6 +142,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
/**
* A callback used to format the counter text.
* By default the counter text is set to "itemLength / maxLength".
* @deprecated Use the `counterFormatter` property on `ion-input` or `ion-textarea` instead.
*/
@Prop() counterFormatter?: CounterFormatter;

Expand Down Expand Up @@ -207,7 +209,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
}

componentDidLoad() {
const { el } = this;
const { el, counter, counterFormatter } = this;
const hasHelperSlot = el.querySelector('[slot="helper"]') !== null;
if (hasHelperSlot) {
printIonWarning(
Expand All @@ -224,6 +226,20 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
);
}

if (counter === true) {
printIonWarning(
'The "counter" property has been deprecated in favor of using the "counter" property on ion-input or ion-textarea.',
el
);
}

if (counterFormatter !== undefined) {
printIonWarning(
'The "counterFormatter" property has been deprecated in favor of using the "counterFormatter" property on ion-input or ion-textarea.',
el
);
}

raf(() => {
this.inheritedAriaAttributes = inheritAttributes(el, ['aria-label']);
this.setMultipleInputs();
Expand Down