feat(range): add label prop (#27408)

Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Labels on `ion-range` can only be set via the `label` slot. When only
plain text is needed, this is cumbersome because you need to add an
entire new element to wrap the label.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

Label prop added. If both the prop and slot are used, the prop will take
priority.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

- Docs PR: https://github.com/ionic-team/ionic-docs/pull/2955
- Dev build: `7.0.6-dev.11683657201.139d03f4`
This commit is contained in:
Amanda Johnston
2023-05-10 10:13:26 -05:00
committed by GitHub
parent 1c71bfb327
commit 368add2a5c
47 changed files with 98 additions and 13 deletions

View File

@ -1619,14 +1619,14 @@ export declare interface IonRadioGroup extends Components.IonRadioGroup {
@ProxyCmp({
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'labelPlacement', 'legacy', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'label', 'labelPlacement', 'legacy', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
})
@Component({
selector: 'ion-range',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'labelPlacement', 'legacy', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value'],
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'label', 'labelPlacement', 'legacy', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value'],
})
export class IonRange {
protected el: HTMLElement;

View File

@ -1030,6 +1030,7 @@ ion-range,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secon
ion-range,prop,debounce,number | undefined,undefined,false,false
ion-range,prop,disabled,boolean,false,false,false
ion-range,prop,dualKnobs,boolean,false,false,false
ion-range,prop,label,string | undefined,undefined,false,false
ion-range,prop,labelPlacement,"end" | "fixed" | "start",'start',false,false
ion-range,prop,legacy,boolean | undefined,undefined,false,false
ion-range,prop,max,number,100,false,false

View File

@ -2289,6 +2289,10 @@ export namespace Components {
* Show two knobs.
*/
"dualKnobs": boolean;
/**
* The text to display as the control's label. Use this over the `label` slot if you only need plain text. The `label` property will take priority over the `label` slot if both are used.
*/
"label"?: string;
/**
* Where to place the label relative to the range. `"start"`: The label will appear to the left of the range in LTR and to the right in RTL. `"end"`: The label will appear to the right of the range in LTR and to the left in RTL. `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/
@ -6304,6 +6308,10 @@ declare namespace LocalJSX {
* Show two knobs.
*/
"dualKnobs"?: boolean;
/**
* The text to display as the control's label. Use this over the `label` slot if you only need plain text. The `label` property will take priority over the `label` slot if both are used.
*/
"label"?: string;
/**
* Where to place the label relative to the range. `"start"`: The label will appear to the left of the range in LTR and to the right in RTL. `"end"`: The label will appear to the right of the range in LTR and to the left in RTL. `"fixed"`: The label has the same behavior as `"start"` except it also has a fixed width. Long text will be truncated with ellipses ("...").
*/

View File

@ -22,8 +22,7 @@
font-size: $range-md-pin-font-size;
}
// TODO FW-2997 Remove this
::slotted([slot="label"]) {
::slotted([slot="label"]), .label-text {
font-size: initial;
}

View File

@ -209,7 +209,7 @@
opacity: 0.3;
}
::slotted([slot="label"]) {
::slotted([slot="label"]), .label-text {
/**
* Label text should not extend
* beyond the bounds of the range.

View File

@ -96,6 +96,13 @@ export class Range implements ComponentInterface {
*/
@Prop() name = this.rangeId;
/**
* The text to display as the control's label. Use this over the `label` slot if
* you only need plain text. The `label` property will take priority over the
* `label` slot if both are used.
*/
@Prop() label?: string;
/**
* Show two knobs.
*/
@ -602,7 +609,7 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
}
private renderRange() {
const { disabled, el, rangeId, pin, pressedKnob, labelPlacement } = this;
const { disabled, el, rangeId, pin, pressedKnob, labelPlacement, label } = this;
const mode = getIonMode(this);
@ -629,7 +636,7 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
'label-text-wrapper-hidden': !this.hasLabel,
}}
>
<slot name="label"></slot>
{label !== undefined ? <div class="label-text">{label}</div> : <slot name="label"></slot>}
</div>
<div class="native-wrapper">
<slot name="start"></slot>
@ -642,7 +649,7 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
}
private get hasLabel() {
return this.el.querySelector('[slot="label"]') !== null;
return this.label !== undefined || this.el.querySelector('[slot="label"]') !== null;
}
private renderRangeSlider() {

View File

@ -15,14 +15,18 @@
<main>
<h1>Range - a11y</h1>
<ion-range><span slot="label">my label</span></ion-range
><br />
<ion-range aria-label="my aria label"></ion-range><br />
<ion-range><span slot="label">my label</span></ion-range>
<br />
<ion-range label="my label"></ion-range>
<br />
<ion-range aria-label="my aria label"></ion-range>
<br />
<ion-range>
<span slot="label">temperature</span>
<ion-icon name="snow" slot="start" aria-hidden="true"></ion-icon>
<ion-icon name="flame" slot="end" aria-hidden="true"></ion-icon> </ion-range
><br />
<ion-icon name="flame" slot="end" aria-hidden="true"></ion-icon>
</ion-range>
<br />
</main>
</body>
</html>

View File

@ -67,6 +67,22 @@
</div>
</div>
<h1>Label Property</h1>
<div class="grid">
<div class="grid-item">
<h2>Placement Start</h2>
<ion-range label="Temperature"></ion-range>
</div>
<div class="grid-item">
<h2>Placement End</h2>
<ion-range label="Temperature" label-placement="end"></ion-range>
</div>
<div class="grid-item">
<h2>Placement Fixed</h2>
<ion-range label="Temperature" label-placement="fixed"></ion-range>
</div>
</div>
<h1>Slotted Items</h1>
<div class="grid">
<div class="grid-item">

View File

@ -125,6 +125,32 @@ configs().forEach(({ title, screenshot, config }) => {
expect(await range.screenshot()).toMatchSnapshot(screenshot(`range-items-fixed`));
});
});
test.describe('range: label prop', () => {
test('should render label in the start placement', async ({ page }) => {
await page.setContent(`<ion-range label-placement="start" label="Volume"></ion-range>`, config);
const range = page.locator('ion-range');
expect(await range.screenshot()).toMatchSnapshot(screenshot(`range-label-prop-start`));
});
test('should render label in the end placement', async ({ page }) => {
await page.setContent(`<ion-range label-placement="end" label="Volume"></ion-range>`, config);
const range = page.locator('ion-range');
expect(await range.screenshot()).toMatchSnapshot(screenshot(`range-label-prop-end`));
});
test('should render label in the fixed placement', async ({ page }) => {
await page.setContent(`<ion-range label-placement="fixed" label="Volume"></ion-range>`, config);
const range = page.locator('ion-range');
expect(await range.screenshot()).toMatchSnapshot(screenshot(`range-label-prop-fixed`));
});
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,23 @@
import { newSpecPage } from '@stencil/core/testing';
import { Range } from '../../range';
describe('range: label', () => {
it('should prioritize the label prop over the slot', async () => {
const page = await newSpecPage({
components: [Range],
html: `
<ion-range label="Label prop">
<div slot="label">Label slot</div>
</ion-range>
`,
});
const range = page.body.querySelector('ion-range');
const propEl = range?.shadowRoot?.querySelector('.label-text');
const slotEl = range?.shadowRoot?.querySelector('slot[name="label"]');
expect(propEl).not.toBeNull();
expect(slotEl).toBeNull();
});
});

View File

@ -606,6 +606,7 @@ export const IonRange = /*@__PURE__*/ defineContainer<JSX.IonRange, JSX.IonRange
'color',
'debounce',
'name',
'label',
'dualKnobs',
'min',
'max',