diff --git a/angular/bootstrap/src/components/slider/slider.component.ts b/angular/bootstrap/src/components/slider/slider.component.ts
index 628bdb6705..82242f53c1 100644
--- a/angular/bootstrap/src/components/slider/slider.component.ts
+++ b/angular/bootstrap/src/components/slider/slider.component.ts
@@ -14,12 +14,14 @@ import {
Directive,
TemplateRef,
ViewEncapsulation,
+ afterRenderEffect,
+ contentChild,
forwardRef,
inject,
input,
output,
+ signal,
viewChild,
- contentChild,
} from '@angular/core';
import {NG_VALUE_ACCESSOR} from '@angular/forms';
import {callWidgetFactory} from '../../config';
@@ -56,12 +58,48 @@ export class SliderHandleDirective {
imports: [UseDirective, SliderHandleDirective],
template: `
-
+
`,
})
class SliderDefaultHandleSlotComponent {
readonly handle = viewChild.required>('handle');
+
+ /**
+ * When the element moves up the DOM Angluar removes the focus
+ * To avoid this we need to manually focus the element by calling the HTMLElement.focus
+ * after the change, afterRenderEffect is executed after each render of the component
+ * and when the signal inside is changed. On each key stroke we set the `refocus` signal
+ * to the element that needs to be focused and we put the `equal` function to always return
+ * `false` in order to trigger the change even when the element is the same
+ */
+ readonly refocus = signal(undefined, {equal: () => false});
+
+ constructor() {
+ afterRenderEffect(() => {
+ this.refocus()?.focus();
+ });
+ }
+
+ /**
+ * Key handler that sets the refocus element only on the key strokes that move
+ * the element up the DOM
+ * @param event object containting key stroke and the target element
+ */
+ onKeyDown(event: KeyboardEvent) {
+ switch (event.key) {
+ case 'ArrowDown':
+ case 'ArrowLeft':
+ case 'Home':
+ case 'ArrowUp':
+ case 'ArrowRight':
+ case 'End':
+ this.refocus.set(event.target as HTMLElement);
+ break;
+ default:
+ break;
+ }
+ }
}
/**
diff --git a/core/src/components/slider/slider.ts b/core/src/components/slider/slider.ts
index 6bd57ef5c1..31afe49818 100644
--- a/core/src/components/slider/slider.ts
+++ b/core/src/components/slider/slider.ts
@@ -726,7 +726,6 @@ export function createSlider(config?: PropsConfig): SliderWidget {
return;
}
event.preventDefault();
- event.stopPropagation();
}
},
mousedown: (event: MouseEvent) => {
diff --git a/e2e/slider/slider.e2e-spec.ts b/e2e/slider/slider.e2e-spec.ts
index 739d495b00..d27d128176 100644
--- a/e2e/slider/slider.e2e-spec.ts
+++ b/e2e/slider/slider.e2e-spec.ts
@@ -334,43 +334,17 @@ test.describe(`Slider tests`, () => {
expect((await sliderPO.sliderProgressState())[0]).toEqual('left: 40%; width: 43%; height: 100%;');
});
- test(`should move handle on key strokes`, async ({page}) => {
+ test(`should move handle on key strokes and keep the focus`, async ({page}) => {
const sliderPO = new SliderPO(page, 1);
await page.goto('#/slider/range');
await sliderPO.locatorRoot.waitFor();
const expectedState = {...defaultExpectedHandleState};
- assign(expectedState, [
- {
- ...expectedState[0],
- value: '0',
- ariaLabel: '0',
- ariaValueText: '0',
- style: 'left: 0%;',
- },
- {
- ...expectedState[1],
- value: '40',
- ariaLabel: '40',
- ariaValueText: '40',
- style: 'left: 40%;',
- },
- ]);
-
const minLabelLocator = sliderPO.locatorMinLabelHorizontal;
const maxLabelLocator = sliderPO.locatorMaxLabelHorizontal;
await sliderPO.locatorHandle.nth(0).click();
- await page.keyboard.press('Home');
-
- await expect.poll(async () => (await sliderPO.sliderHandleState()).at(0)).toEqual(expectedState[0]);
- expect((await sliderPO.sliderHandleState()).at(1)).toEqual(expectedState[1]);
- expect((await sliderPO.sliderProgressState())[0]).toEqual('left: 0%; width: 40%; height: 100%;');
-
- await expect(minLabelLocator).toBeHidden();
- await expect(maxLabelLocator).toBeVisible();
-
await page.keyboard.press('End');
assign(expectedState, [
@@ -394,8 +368,36 @@ test.describe(`Slider tests`, () => {
expect((await sliderPO.sliderHandleState()).at(1)).toEqual(expectedState[1]);
expect((await sliderPO.sliderProgressState())[0]).toEqual('left: 40%; width: 60%; height: 100%;');
+ await expect(sliderPO.locatorHandle.nth(1)).toBeFocused();
await expect(minLabelLocator).toBeVisible();
await expect(maxLabelLocator).toBeHidden();
+
+ await page.keyboard.press('Home');
+
+ assign(expectedState, [
+ {
+ ...expectedState[0],
+ value: '0',
+ ariaLabel: '0',
+ ariaValueText: '0',
+ style: 'left: 0%;',
+ },
+ {
+ ...expectedState[1],
+ value: '40',
+ ariaLabel: '40',
+ ariaValueText: '40',
+ style: 'left: 40%;',
+ },
+ ]);
+
+ await expect.poll(async () => (await sliderPO.sliderHandleState()).at(0)).toEqual(expectedState[0]);
+ expect((await sliderPO.sliderHandleState()).at(1)).toEqual(expectedState[1]);
+ expect((await sliderPO.sliderProgressState())[0]).toEqual('left: 0%; width: 40%; height: 100%;');
+
+ await expect(sliderPO.locatorHandle.nth(0)).toBeFocused();
+ await expect(minLabelLocator).toBeHidden();
+ await expect(maxLabelLocator).toBeVisible();
});
test(`should add / remove combined label from dom`, async ({page}) => {
diff --git a/svelte/bootstrap/src/components/slider/SliderDefaultHandle.svelte b/svelte/bootstrap/src/components/slider/SliderDefaultHandle.svelte
index 3934c0dfba..12cd7ce25b 100644
--- a/svelte/bootstrap/src/components/slider/SliderDefaultHandle.svelte
+++ b/svelte/bootstrap/src/components/slider/SliderDefaultHandle.svelte
@@ -2,7 +2,43 @@
import type {SliderSlotHandleContext} from './slider.gen';
let {item, directives}: SliderSlotHandleContext = $props();
+
+ let refocusElement: HTMLElement | undefined = $state(undefined);
+ let updateTimeout: NodeJS.Timeout;
+
+ // Manually keep focus as scheduling goal elements are re-ordered.
+ // Svelte currently does not retain focus as elements are moved, even when keyed.
+ // See discussion here: https://github.com/sveltejs/svelte/issues/3973
+ $effect(() => {
+ if (refocusElement) {
+ updateTimeout = setTimeout(() => {
+ refocusElement?.focus();
+ refocusElement = undefined;
+ });
+ }
+ });
+
+ /**
+ * Key handler that sets the refocus element only on the key strokes that move
+ * the element up the DOM
+ * @param event object containting key stroke and the target element
+ */
+ function onkeydown(event: KeyboardEvent) {
+ switch (event.key) {
+ case 'ArrowUp':
+ case 'ArrowRight':
+ case 'End':
+ case 'ArrowDown':
+ case 'ArrowLeft':
+ case 'Home':
+ refocusElement = event.target as HTMLElement;
+ clearTimeout(updateTimeout);
+ break;
+ default:
+ break;
+ }
+ }
-
+