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);
+ });
+});