diff --git a/demo/e2e/screenshot.spec.ts-snapshots/inputs-dark.png b/demo/e2e/screenshot.spec.ts-snapshots/inputs-dark.png index 9fbfbcf..03b529c 100644 Binary files a/demo/e2e/screenshot.spec.ts-snapshots/inputs-dark.png and b/demo/e2e/screenshot.spec.ts-snapshots/inputs-dark.png differ diff --git a/demo/e2e/screenshot.spec.ts-snapshots/inputs.png b/demo/e2e/screenshot.spec.ts-snapshots/inputs.png index 2cc0727..5267ba4 100644 Binary files a/demo/e2e/screenshot.spec.ts-snapshots/inputs.png and b/demo/e2e/screenshot.spec.ts-snapshots/inputs.png differ diff --git a/demo/src/app/index/pages/inputs/inputs.page.html b/demo/src/app/index/pages/inputs/inputs.page.html index 6b889bb..a6da813 100644 --- a/demo/src/app/index/pages/inputs/inputs.page.html +++ b/demo/src/app/index/pages/inputs/inputs.page.html @@ -40,6 +40,14 @@

inputs

+ + + + + + + + @@ -62,6 +70,67 @@

inputs

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/src/app/index/pages/inputs/inputs.page.spec.ts b/demo/src/app/index/pages/inputs/inputs.page.spec.ts index 0f6891c..df2f299 100644 --- a/demo/src/app/index/pages/inputs/inputs.page.spec.ts +++ b/demo/src/app/index/pages/inputs/inputs.page.spec.ts @@ -18,4 +18,38 @@ describe('InputsPage', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should render helper and error input patterns', () => { + const element = fixture.nativeElement as HTMLElement; + const helperInput = element.querySelector('ion-input[helpertext]'); + const errorInput = element.querySelector('ion-input[errortext]'); + const helperTextarea = element.querySelector('ion-textarea[helpertext]'); + const errorTextarea = element.querySelector('ion-textarea[errortext]'); + + expect(helperInput?.getAttribute('helpertext')).toBe('This is helper text'); + expect(errorInput?.getAttribute('errortext')).toBe('This is error text'); + expect(errorInput?.classList.contains('ion-invalid')).toBe(true); + expect(errorInput?.classList.contains('ion-touched')).toBe(true); + expect(helperTextarea?.getAttribute('helpertext')).toBe('This is helper text'); + expect(errorTextarea?.getAttribute('errortext')).toBe('This is error text'); + expect(errorTextarea?.classList.contains('ion-invalid')).toBe(true); + expect(errorTextarea?.classList.contains('ion-touched')).toBe(true); + }); + + it('should render outlined helper and error patterns', () => { + const element = fixture.nativeElement as HTMLElement; + const outlinedInputs = element.querySelectorAll('ion-input[fill="outline"]'); + const outlinedTextareas = element.querySelectorAll('ion-textarea[fill="outline"]'); + + expect(outlinedInputs).toHaveLength(4); + expect(outlinedInputs[2].getAttribute('helpertext')).toBe('This is helper text'); + expect(outlinedInputs[3].getAttribute('errortext')).toBe('This is error text'); + expect(outlinedInputs[3].classList.contains('ion-invalid')).toBe(true); + expect(outlinedInputs[3].classList.contains('ion-touched')).toBe(true); + expect(outlinedTextareas).toHaveLength(4); + expect(outlinedTextareas[2].getAttribute('helpertext')).toBe('This is helper text'); + expect(outlinedTextareas[3].getAttribute('errortext')).toBe('This is error text'); + expect(outlinedTextareas[3].classList.contains('ion-invalid')).toBe(true); + expect(outlinedTextareas[3].classList.contains('ion-touched')).toBe(true); + }); }); diff --git a/demo/src/global.scss b/demo/src/global.scss index 5a1bba3..d7bf220 100644 --- a/demo/src/global.scss +++ b/demo/src/global.scss @@ -71,6 +71,12 @@ html.ionic-v8 { &:is(.textarea-label-placement-stacked, .textarea-label-placement-floating) { block-size: 68px; } + + &.textarea-fill-outline .textarea-wrapper { + block-size: 124px; + box-sizing: border-box; + padding-top: 12px; + } } } diff --git a/src/styles/components/ion-inputs.scss b/src/styles/components/ion-inputs.scss new file mode 100644 index 0000000..06f8e6c --- /dev/null +++ b/src/styles/components/ion-inputs.scss @@ -0,0 +1,82 @@ +@mixin outline-field($control) { + --placeholder-opacity: 1; + --padding-top: 12px; + --padding-bottom: 0; + margin-bottom: 12px; + + .#{$control}-control, + .#{$control}-wrapper { + flex-direction: column; + } + + .label-text-wrapper { + max-width: 100%; + margin: 0 2px 4px; + padding: 0; + font-size: 0.875rem; + line-height: 1.25rem; + transform: none; + } + + .native-wrapper { + min-height: 48px; + border: 1px solid + var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-250, var(--ion-background-color-step-250, #c8c7cc)))); + border-radius: 10px; + background: var(--ion-background-color, #fff); + transition: border-color 150ms ease; + } + + .#{$control}-bottom { + inset-inline: 0; + padding: 4px 2px 0; + border: 0; + font-size: 0.75rem; + line-height: 1rem; + } + + &.has-focus { + .label-text-wrapper { + color: var(--highlight-color-focused); + } + + .native-wrapper { + border-color: var(--highlight-color-focused); + } + } + + &.ion-touched.ion-invalid .native-wrapper { + border-color: var(--highlight-color-invalid); + } + + &.has-focus.ion-touched.ion-invalid .label-text-wrapper { + color: var(--highlight-color-invalid); + } +} + +ion-input.ios:not(.ios26-disabled) { + &.input-fill-outline { + @include outline-field('input'); + + .native-input { + min-height: 46px; + padding-inline: 12px; + } + } +} + +ion-textarea.ios:not(.ios26-disabled) { + &.textarea-fill-outline { + @include outline-field('textarea'); + + .native-wrapper { + min-height: 88px; + } + + .native-textarea { + min-height: 86px; + padding-block: 12px; + padding-inline: 12px; + } + } +} diff --git a/src/styles/ionic-theme-ios26.scss b/src/styles/ionic-theme-ios26.scss index 97e5dc1..48dc0af 100644 --- a/src/styles/ionic-theme-ios26.scss +++ b/src/styles/ionic-theme-ios26.scss @@ -7,6 +7,7 @@ @use 'components/ion-content'; @use 'components/ion-datetime'; @use 'components/ion-fab'; +@use 'components/ion-inputs'; @use 'components/ion-list'; @use 'components/ion-loading'; @use 'components/ion-menu'; diff --git a/src/styles/utils/structured-list.scss b/src/styles/utils/structured-list.scss index 07b1dee..8541fbe 100644 --- a/src/styles/utils/structured-list.scss +++ b/src/styles/utils/structured-list.scss @@ -63,6 +63,18 @@ } ion-item { + &:has(ion-input.input-fill-outline), + &:has(ion-textarea.textarea-fill-outline), + &:has(ion-input .input-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty))), + &:has(ion-textarea .textarea-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty))) { + --inner-border-width: 0; + --inner-padding-bottom: 4px; + } + ion-input .input-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty)))), + ion-textarea .textarea-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty)))) { + display: none; + } + &:has(> ion-label:not([slot])):has(> ion-note:not([slot])) { &::part(container) { flex-direction: column;