Skip to content

feat(range): add classes and expose parts to allow individual styling of dual knobs#30941

Open
brandyscarney wants to merge 32 commits intofeature-8.8from
FW-6582
Open

feat(range): add classes and expose parts to allow individual styling of dual knobs#30941
brandyscarney wants to merge 32 commits intofeature-8.8from
FW-6582

Conversation

@brandyscarney
Copy link
Member

@brandyscarney brandyscarney commented Jan 29, 2026

Issue number: resolves #29862


What is the current behavior?

Range exposes a single part for both knobs & pins. This makes it impossible to style the knobs/pins differently when dual knobs is enabled.

What is the new behavior?

  • Fixes a bug where the knobs would swap A & B when they cross over each other
  • Fixes the focus behavior so that dual knobs act the same as a single knob range when focusing a knob
  • Adds the following classes to the host element when dualKnobs is enabled:
    • range-dual-knobs
    • range-pressed-a when the knob with name A is pressed
    • range-pressed-b when the knob with name B is pressed
    • range-pressed-lower when the lower knob is pressed
    • range-pressed-upper when the upper knob is pressed
  • Adds parts for the following:
    • knob-handle-a — The container for the knob with the static A identity when dualKnobs is true. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
    • knob-handle-b — The container for the knob with the static B identity when dualKnobs is true. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
    • knob-handle-lower — The container for the knob whose current value is lower when dualKnobs is true. The lower and upper parts swap which knob handle they refer to when the knobs cross.
    • knob-handle-upper — The container for the knob whose current value is upper when dualKnobs is true. The lower and upper parts swap which knob handle they refer to when the knobs cross.
    • pin-a — The value indicator above the knob with the static A identity when dualKnobs is true. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
    • pin-b — The value indicator above the knob with the static B identity when dualKnobs is true. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
    • pin-lower — The value indicator above the knob whose current value is lower when dualKnobs is true. The lower and upper parts swap which pin they refer to when the knobs cross.
    • pin-upper — The value indicator above the knob whose current value is upper when dualKnobs is true. The lower and upper parts swap which pin they refer to when the knobs cross.
    • knob-a — The visual knob for the static A identity when dualKnobs is true. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
    • knob-b — The visual knob for the static B identity when dualKnobs is true. This identity does not change, even if the knobs cross and swap which one represents the lower or upper value.
    • knob-lower — The visual knob whose current value is lower when dualKnobs is true. The lower and upper parts swap which knob they refer to when the knobs cross.
    • knob-upper — The visual knob whose current value is upper when dualKnobs is true. The lower and upper parts swap which knob they refer to when the knobs cross.
    • activated — Added to the knob-handle, knob, and pin when the knob is active. Only one set has this part at a time when dualKnobs is true.
    • focused — Added to the knob-handle, knob, and pin that currently has focus. Only one set has this part at a time when dualKnobs is true.
    • hover — Added to the knob-handle, knob, and pin when the knob has hover. Only one set has this part at a time when dualKnobs is true.
    • pressed — Added to the knob-handle, knob, and pin that is currently being pressed to drag. Only one set has this part at a time when dualKnobs is true.
  • Adds e2e tests for the following:
    • customizing label part
    • customizing bar parts
    • customizing pin parts
    • customizing tick parts
    • customizing knob parts
    • customizing dual knob a & b parts
    • customizing dual knob lower & upper parts
    • verifies that a & b parts stay on the original elements but lower & upper parts swap when the values swap
  • Adds spec tests for the following:
    • css classes
      • value state classes
      • boolean property classes
      • pressed state classes
    • shadow parts
      • static shadow parts
        • verifies the shadow parts exist based on the existence of certain range properties
      • state shadow parts
        • verifies the shadow parts exist based on the state of the range knob (pressed, focused, activated, hover)

Does this introduce a breaking change?

  • Yes
  • No

Other information

Dev build: 8.7.17-dev.11771959013.18adabe2

@vercel
Copy link

vercel bot commented Jan 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ionic-framework Ready Ready Preview, Comment Feb 27, 2026 10:42pm

Request Review

@github-actions github-actions bot added the package: core @ionic/core package label Jan 29, 2026
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is required otherwise it won't match knob when it has knob knob-a.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed the classes assigned to knob-handle from range-knob-a to range-knob-handle-a to match what they are actually applied to.

configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('range: customization'), () => {
test('should be customizable', async ({ page }) => {
await page.goto(`/src/components/range/test/custom`, config);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the screenshots here in favor of checking that the styles are applied to the right element when the part is styled.

@brandyscarney brandyscarney changed the title feat(range): add class and expose parts for dual knobs for easier styling feat(range): add class and expose parts for dual knobs for custom styling Jan 30, 2026
Comment on lines 53 to 56
* @part activated - Added to the knob-handle, knob, and pin when the knob is activated (has the `ion-activated` class). Only one set has this part at a time when `dualKnobs` is `true`.
* @part focused - Added to the knob-handle, knob, and pin that currently has focus. Only one set has this part at a time when `dualKnobs` is `true`.
* @part hover - Added to the knob-handle, knob, and pin when the knob has hover. Only one set has this part at a time when `dualKnobs` is `true`.
* @part pressed - Added to the knob-handle, knob, and pin that is currently being pressed to drag. Only one set has this part at a time when `dualKnobs` is `true`.
Copy link
Member Author

@brandyscarney brandyscarney Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other components we add this as the part it works with like this:

* @part wheel-item active - The currently selected wheel-item.
* @part time-button active - The time picker button when the picker is open.

but it seemed like a lot to add all of these:

Details
/**
 * @part knob-handle - The container element that wraps the knob and handles drag interactions.
 * @part knob-handle activated - The activated knob-handle. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle focused - The focused knob-handle. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle hover - The hovered knob-handle. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle pressed - The pressed knob-handle. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part knob-handle-a - The container element for the first knob. Only available when `dualKnobs` is `true`.
 * @part knob-handle-a activated - The activated knob-handle-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-a focused - The focused knob-handle-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-a hover - The hovered knob-handle-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-a pressed - The pressed knob-handle-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part knob-handle-b - The container element for the second knob. Only available when `dualKnobs` is `true`.
 * @part knob-handle-b activated - The activated knob-handle-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-b focused - The focused knob-handle-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-b hover - The hovered knob-handle-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-b pressed - The pressed knob-handle-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part knob-handle-lower - The container element for the lower knob. Only available when `dualKnobs` is `true`.
 * @part knob-handle-lower activated - The activated knob-handle-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-lower focused - The focused knob-handle-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-lower hover - The hovered knob-handle-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-lower pressed - The pressed knob-handle-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part knob-handle-upper - The container element for the upper knob. Only available when `dualKnobs` is `true`.
 * @part knob-handle-upper activated - The activated knob-handle-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-upper focused - The focused knob-handle-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-upper hover - The hovered knob-handle-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-handle-upper pressed - The pressed knob-handle-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part pin - The counter that appears above a knob.
 * @part pin activated - The activated pin. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin focused - The focused pin. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin hover - The hovered pin. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin pressed - The pressed pin. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part pin-a - The counter that appears above the first knob. Only available when `dualKnobs` is `true`.
 * @part pin-a activated - The activated pin-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-a focused - The focused pin-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-a hover - The hovered pin-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-a pressed - The pressed pin-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part pin-b - The counter that appears above the second knob. Only available when `dualKnobs` is `true`.
 * @part pin-b activated - The activated pin-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-b focused - The focused pin-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-b hover - The hovered pin-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-b pressed - The pressed pin-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part pin-lower - The counter that appears above the lower knob. Only available when `dualKnobs` is `true`.
 * @part pin-lower activated - The activated pin-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-lower focused - The focused pin-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-lower hover - The hovered pin-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-lower pressed - The pressed pin-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part pin-upper - The counter that appears above the upper knob. Only available when `dualKnobs` is `true`.
 * @part pin-upper activated - The activated pin-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-upper focused - The focused pin-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-upper hover - The hovered pin-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part pin-upper pressed - The pressed pin-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part knob - The visual knob element that appears on the range track.
 * @part knob activated - The activated knob. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob focused - The focused knob. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob hover - The hovered knob. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob pressed - The pressed knob. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part knob-a - The visual knob element for the first knob. Only available when `dualKnobs` is `true`.
 * @part knob-a activated - The activated knob-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-a focused - The focused knob-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-a hover - The hovered knob-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-a pressed - The pressed knob-a. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part knob-b - The visual knob element for the second knob. Only available when `dualKnobs` is `true`.
 * @part knob-b activated - The activated knob-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-b focused - The focused knob-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-b hover - The hovered knob-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-b pressed - The pressed knob-b. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part knob-lower - The visual knob element for the lower knob. Only available when `dualKnobs` is `true`.
 * @part knob-lower activated - The activated knob-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-lower focused - The focused knob-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-lower hover - The hovered knob-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-lower pressed - The pressed knob-lower. Only one set has this part at a time when `dualKnobs` is `true`.
 * 
 * @part knob-upper - The visual knob element for the upper knob. Only available when `dualKnobs` is `true`.
 * @part knob-upper activated - The activated knob-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-upper focused - The focused knob-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-upper hover - The hovered knob-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 * @part knob-upper pressed - The pressed knob-upper. Only one set has this part at a time when `dualKnobs` is `true`.
 */

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote to not include them. Anyone who works with parts are most likely familiar with how to group them so we shouldn't need the extra docs.

@brandyscarney brandyscarney changed the title feat(range): add classes and expose parts for dual knobs for custom styling feat(range): add classes and expose parts to allow individual styling of dual knobs Feb 24, 2026
@brandyscarney brandyscarney marked this pull request as ready for review February 24, 2026 17:02
@brandyscarney brandyscarney requested a review from a team as a code owner February 24, 2026 17:02
Comment on lines 51 to 52
* @part knob-lower - The visual knob element for the lower knob. Only available when `dualKnobs` is `true`.
* @part knob-upper - The visual knob element for the upper knob. Only available when `dualKnobs` is `true`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be beneficial to elaborate what lower and upper mean in terms of values.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* @part knob-b - The visual knob element for the second knob. Only available when `dualKnobs` is `true`.
* @part knob-lower - The visual knob element for the lower knob. Only available when `dualKnobs` is `true`.
* @part knob-upper - The visual knob element for the upper knob. Only available when `dualKnobs` is `true`.
* @part activated - Added to the knob-handle, knob, and pin when the knob is activated (has the `ion-activated` class). Only one set has this part at a time when `dualKnobs` is `true`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to point out that it has the ion-activated class? It feels like too much info.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 53 to 56
* @part activated - Added to the knob-handle, knob, and pin when the knob is activated (has the `ion-activated` class). Only one set has this part at a time when `dualKnobs` is `true`.
* @part focused - Added to the knob-handle, knob, and pin that currently has focus. Only one set has this part at a time when `dualKnobs` is `true`.
* @part hover - Added to the knob-handle, knob, and pin when the knob has hover. Only one set has this part at a time when `dualKnobs` is `true`.
* @part pressed - Added to the knob-handle, knob, and pin that is currently being pressed to drag. Only one set has this part at a time when `dualKnobs` is `true`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote to not include them. Anyone who works with parts are most likely familiar with how to group them so we shouldn't need the extra docs.

Comment on lines 78 to 79
private focusFromPointer = false;
private activatedObserver?: MutationObserver;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add short comments for what these variables are for. It'll be great to be able to look at them and immediately know their purpose instead of reading through the code. Especially since they seem like they do complex work.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'range-knob-max': value === max,
'ion-activatable': true,
'ion-focusable': true,
'ion-focused': focused,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't clash with ion-focusable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we were already manually adding the ion-focused class to the dual knobs, I am just doing it directly on the element instead:

// Manually manage ion-focused class for dual knobs
if (this.dualKnobs && this.el.shadowRoot) {
const knobA = this.el.shadowRoot.querySelector('.range-knob-a');
const knobB = this.el.shadowRoot.querySelector('.range-knob-b');
// Remove ion-focused from both knobs first
knobA?.classList.remove('ion-focused');
knobB?.classList.remove('ion-focused');
// Add ion-focused only to the focused knob
const focusedKnobEl = knob === 'A' ? knobA : knobB;
focusedKnobEl?.classList.add('ion-focused');
}

// Remove ion-focused from both knobs when focus leaves the range
if (this.dualKnobs && this.el.shadowRoot) {
const knobA = this.el.shadowRoot.querySelector('.range-knob-a');
const knobB = this.el.shadowRoot.querySelector('.range-knob-b');
knobA?.classList.remove('ion-focused');
knobB?.classList.remove('ion-focused');
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants