diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 55fabe7a3e6..f888b36dca7 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2698,7 +2698,7 @@ export namespace Components { */ "justify": 'start' | 'end' | 'space-between'; /** - * The visible label associated with the select. + * The visible label associated with the select. Use this if you need to render a plaintext label. The `label` property will take priority over the `label` slot if both are used. */ "label"?: string; /** @@ -6772,7 +6772,7 @@ declare namespace LocalJSX { */ "justify"?: 'start' | 'end' | 'space-between'; /** - * The visible label associated with the select. + * The visible label associated with the select. Use this if you need to render a plaintext label. The `label` property will take priority over the `label` slot if both are used. */ "label"?: string; /** diff --git a/core/src/components/select/select.scss b/core/src/components/select/select.scss index ef32a431950..2128ca099e4 100644 --- a/core/src/components/select/select.scss +++ b/core/src/components/select/select.scss @@ -294,7 +294,8 @@ button { * works on block-level elements. A flex item is * considered blockified (https://www.w3.org/TR/css-display-3/#blockify). */ -.label-text { +.label-text, +::slotted([slot="label"]) { text-overflow: ellipsis; white-space: nowrap; @@ -302,6 +303,15 @@ button { overflow: hidden; } +/** + * If no label text is placed into the slot + * then the element should be hidden otherwise + * there will be additional margins added. + */ +.label-text-wrapper-hidden { + display: none; +} + // Select Native Wrapper // ---------------------------------------------------------------- diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 5931915cf31..4fb6fe5bdac 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -32,9 +32,12 @@ import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from ' /** * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. * + * @slot label - label - The label text to associate with the select. Use the "labelPlacement" property to control where the label is placed relative to the select. Use this if you need to render a label with custom HTML. + * * @part placeholder - The text displayed in the select when there is no value. * @part text - The displayed value of the select. * @part icon - The select icon container. + * */ @Component({ tag: 'ion-select', @@ -122,6 +125,10 @@ export class Select implements ComponentInterface { /** * The visible label associated with the select. + * + * Use this if you need to render a plaintext label. + * + * The `label` property will take priority over the `label` slot if both are used. */ @Prop() label?: string; @@ -566,7 +573,7 @@ export class Select implements ComponentInterface { * TODO FW-3194 * Remove legacyFormController logic. * Remove label and labelText vars - * Pass `this.label` instead of `labelText` + * Pass `this.labelText` instead of `labelText` * when setting the header. */ let label: HTMLElement | null; @@ -576,7 +583,7 @@ export class Select implements ComponentInterface { label = this.getLabel(); labelText = label ? label.textContent : null; } else { - labelText = this.label; + labelText = this.labelText; } const interfaceOptions = this.interfaceOptions; @@ -649,6 +656,30 @@ export class Select implements ComponentInterface { return Array.from(this.el.querySelectorAll('ion-select-option')); } + /** + * Returns any plaintext associated with + * the label (either prop or slot). + * Note: This will not return any custom + * HTML. Use the `hasLabel` getter if you + * want to know if any slotted label content + * was passed. + */ + private get labelText() { + const { el, label } = this; + + if (label !== undefined) { + return label; + } + + const labelSlot = el.querySelector('[slot="label"]'); + + if (labelSlot !== null) { + return labelSlot.textContent; + } + + return; + } + private getText(): string { const selectedText = this.selectedText; if (selectedText != null && selectedText !== '') { @@ -696,17 +727,29 @@ export class Select implements ComponentInterface { private renderLabel() { const { label } = this; - if (label === undefined) { - return; - } return ( -
-
{this.label}
+
+ {label === undefined ? :
{label}
}
); } + /** + * Returns `true` if label content is provided + * either by a prop or a content. If you want + * to get the plaintext value of the label use + * the `labelText` getter instead. + */ + private get hasLabel() { + return this.label !== undefined || this.el.querySelector('[slot="label"]') !== null; + } + /** * Renders the border container * when fill="outline". @@ -902,10 +945,10 @@ Developers can use the "legacy" property to continue using the legacy form marku } private get ariaLabel() { - const { placeholder, label, el, inputId, inheritedAttributes } = this; + const { placeholder, el, inputId, inheritedAttributes } = this; const displayValue = this.getText(); const { labelText } = getAriaLabel(el, inputId); - const definedLabel = label ?? inheritedAttributes['aria-label'] ?? labelText; + const definedLabel = this.labelText ?? inheritedAttributes['aria-label'] ?? labelText; /** * If developer has specified a placeholder diff --git a/core/src/components/select/test/a11y/index.html b/core/src/components/select/test/a11y/index.html index fd7b9bd55f8..cd859593888 100644 --- a/core/src/components/select/test/a11y/index.html +++ b/core/src/components/select/test/a11y/index.html @@ -15,6 +15,7 @@

Select - a11y

+
Slotted Label




diff --git a/core/src/components/select/test/label/select.e2e.ts b/core/src/components/select/test/label/select.e2e.ts index 1989ffab301..f9c0c570f0a 100644 --- a/core/src/components/select/test/label/select.e2e.ts +++ b/core/src/components/select/test/label/select.e2e.ts @@ -267,7 +267,7 @@ configs().forEach(({ title, screenshot, config }) => { configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { test.describe(title('select: label overflow'), () => { - test('label should be truncated with ellipses', async ({ page }) => { + test('label property should be truncated with ellipses', async ({ page }) => { await page.setContent( ` @@ -278,11 +278,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co const select = page.locator('ion-select'); expect(await select.screenshot()).toMatchSnapshot(screenshot(`select-label-truncate`)); }); + test('label slot should be truncated with ellipses', async ({ page }) => { + await page.setContent( + ` + +
Label Label Label Label Label
+
+ `, + config + ); + + const select = page.locator('ion-select'); + expect(await select.screenshot()).toMatchSnapshot(screenshot(`select-label-slot-truncate`)); + }); }); }); configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { test.describe(title('select: alert label'), () => { - test('should use the label to set the default header in an alert', async ({ page }) => { + test('should use the label prop to set the default header in an alert', async ({ page }) => { await page.setContent( ` @@ -301,5 +314,47 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => await expect(alert.locator('.alert-title')).toHaveText('My Alert'); }); + test('should use the label slot to set the default header in an alert', async ({ page }) => { + await page.setContent( + ` + +
My Alert
+ A +
+ `, + config + ); + + const select = page.locator('ion-select'); + const alert = page.locator('ion-alert'); + const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); + + await select.click(); + await ionAlertDidPresent.next(); + + await expect(alert.locator('.alert-title')).toHaveText('My Alert'); + }); + test('should use the label prop to set the default header in an alert if both prop and slot are set', async ({ + page, + }) => { + await page.setContent( + ` + +
My Slot Alert
+ A +
+ `, + config + ); + + const select = page.locator('ion-select'); + const alert = page.locator('ion-alert'); + const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); + + await select.click(); + await ionAlertDidPresent.next(); + + await expect(alert.locator('.alert-title')).toHaveText('My Prop Alert'); + }); }); }); diff --git a/core/src/components/select/test/label/select.e2e.ts-snapshots/select-label-slot-truncate-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/label/select.e2e.ts-snapshots/select-label-slot-truncate-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..5acafac7d00 Binary files /dev/null and b/core/src/components/select/test/label/select.e2e.ts-snapshots/select-label-slot-truncate-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/label/select.e2e.ts-snapshots/select-label-slot-truncate-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/label/select.e2e.ts-snapshots/select-label-slot-truncate-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6d9f6a7c909 Binary files /dev/null and b/core/src/components/select/test/label/select.e2e.ts-snapshots/select-label-slot-truncate-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/label/select.e2e.ts-snapshots/select-label-slot-truncate-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/label/select.e2e.ts-snapshots/select-label-slot-truncate-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..60b208fee5e Binary files /dev/null and b/core/src/components/select/test/label/select.e2e.ts-snapshots/select-label-slot-truncate-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/select.spec.tsx b/core/src/components/select/test/select.spec.tsx new file mode 100644 index 00000000000..b2fee23d211 --- /dev/null +++ b/core/src/components/select/test/select.spec.tsx @@ -0,0 +1,54 @@ +import { newSpecPage } from '@stencil/core/testing'; + +import { Select } from '../select'; + +describe('ion-select', () => { + it('should render label prop if only prop provided', async () => { + const page = await newSpecPage({ + components: [Select], + html: ` + + `, + }); + + const select = page.body.querySelector('ion-select'); + + const propEl = select.shadowRoot.querySelector('.label-text'); + const slotEl = select.shadowRoot.querySelector('slot[name="label"]'); + + expect(propEl).not.toBe(null); + expect(slotEl).toBe(null); + }); + it('should render label slot if only slot provided', async () => { + const page = await newSpecPage({ + components: [Select], + html: ` +
Label Prop Slot
+ `, + }); + + const select = page.body.querySelector('ion-select'); + + const propEl = select.shadowRoot.querySelector('.label-text'); + const slotEl = select.shadowRoot.querySelector('slot[name="label"]'); + + expect(propEl).toBe(null); + expect(slotEl).not.toBe(null); + }); + it('should render label prop if both prop and slot provided', async () => { + const page = await newSpecPage({ + components: [Select], + html: ` +
Label Prop Slot
+ `, + }); + + const select = page.body.querySelector('ion-select'); + + const propEl = select.shadowRoot.querySelector('.label-text'); + const slotEl = select.shadowRoot.querySelector('slot[name="label"]'); + + expect(propEl).not.toBe(null); + expect(slotEl).toBe(null); + }); +});