From 016dd15ea5af66fa146a3c34b8fe1855119d1e70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:34:03 +0000 Subject: [PATCH 01/26] Add OK basic slider component Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/all-components.ts | 1 + packages/ok-components/src/basic/all-basic.ts | 1 + .../ok-components/src/basic/slider/index.ts | 28 ++++ .../ok-components/src/basic/slider/styles.ts | 158 ++++++++++++++++++ .../src/basic/slider/tests/slider.spec.ts | 22 +++ .../ok/basic/slider/slider-matrix.stories.ts | 60 +++++++ .../storybook/src/ok/basic/slider/slider.mdx | 16 ++ .../src/ok/basic/slider/slider.stories.ts | 94 +++++++++++ 8 files changed, 380 insertions(+) create mode 100644 packages/ok-components/src/basic/all-basic.ts create mode 100644 packages/ok-components/src/basic/slider/index.ts create mode 100644 packages/ok-components/src/basic/slider/styles.ts create mode 100644 packages/ok-components/src/basic/slider/tests/slider.spec.ts create mode 100644 packages/storybook/src/ok/basic/slider/slider-matrix.stories.ts create mode 100644 packages/storybook/src/ok/basic/slider/slider.mdx create mode 100644 packages/storybook/src/ok/basic/slider/slider.stories.ts diff --git a/packages/ok-components/src/all-components.ts b/packages/ok-components/src/all-components.ts index b71fb0dfe4..526d6043fb 100644 --- a/packages/ok-components/src/all-components.ts +++ b/packages/ok-components/src/all-components.ts @@ -6,6 +6,7 @@ import '@ni/spright-components/dist/esm/all-components'; +import './basic/all-basic'; import './fv/all-fv'; import './ex/all-ex'; import './ts/all-ts'; diff --git a/packages/ok-components/src/basic/all-basic.ts b/packages/ok-components/src/basic/all-basic.ts new file mode 100644 index 0000000000..61d26f820f --- /dev/null +++ b/packages/ok-components/src/basic/all-basic.ts @@ -0,0 +1 @@ +import './slider'; diff --git a/packages/ok-components/src/basic/slider/index.ts b/packages/ok-components/src/basic/slider/index.ts new file mode 100644 index 0000000000..a9b091128d --- /dev/null +++ b/packages/ok-components/src/basic/slider/index.ts @@ -0,0 +1,28 @@ +import { + DesignSystem, + Slider as FoundationSlider, + sliderTemplate as template, + type SliderOptions +} from '@ni/fast-foundation'; +import { styles } from './styles'; + +declare global { + interface HTMLElementTagNameMap { + 'ok-basic-slider': Slider; + } +} + +/** + * A Nimble-styled slider control. + */ +export class Slider extends FoundationSlider {} + +const okBasicSlider = Slider.compose({ + baseName: 'basic-slider', + baseClass: FoundationSlider, + template, + styles +}); + +DesignSystem.getOrCreate().withPrefix('ok').register(okBasicSlider()); +export const sliderTag = 'ok-basic-slider'; diff --git a/packages/ok-components/src/basic/slider/styles.ts b/packages/ok-components/src/basic/slider/styles.ts new file mode 100644 index 0000000000..5aba858a29 --- /dev/null +++ b/packages/ok-components/src/basic/slider/styles.ts @@ -0,0 +1,158 @@ +import { css } from '@ni/fast-element'; +import { + applicationBackgroundColor, + bodyDisabledFontColor, + bodyFont, + bodyFontColor, + borderColor, + borderHoverColor, + borderRgbPartialColor, + borderWidth, + fillDownColor, + fillSelectedColor, + smallDelay +} from '@ni/nimble-components/dist/esm/theme-provider/design-tokens'; +import { display } from '../../utilities/style/display'; + +export const styles = css` + @layer base, hover, focusVisible, active, disabled, top; + + @layer base { + ${display('inline-block')} + + :host { + width: 240px; + height: 32px; + outline: none; + color: ${bodyFontColor}; + font: ${bodyFont}; + cursor: pointer; + touch-action: none; + } + + :host([orientation='vertical']) { + width: 32px; + height: 240px; + } + + :host([readonly]) { + cursor: default; + } + + .positioning-region { + position: relative; + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + } + + .track { + position: relative; + width: 100%; + height: 4px; + border-radius: 2px; + background-color: rgba(${borderRgbPartialColor}, 0.3); + } + + .track-start { + position: absolute; + inset-block: 0; + left: 0; + border-radius: inherit; + background-color: ${fillSelectedColor}; + } + + .thumb-container { + position: absolute; + top: 50%; + width: 16px; + height: 16px; + box-sizing: border-box; + border: ${borderWidth} solid ${borderColor}; + border-radius: 50%; + background-color: ${applicationBackgroundColor}; + transform: translate(50%, -50%); + transition: + border-color ${smallDelay} ease-in-out, + box-shadow ${smallDelay} ease-in-out, + background-color ${smallDelay} ease-in-out; + } + + :host([orientation='vertical']) .positioning-region { + align-items: initial; + } + + :host([orientation='vertical']) .track { + width: 4px; + height: 100%; + } + + :host([orientation='vertical']) .track-start { + inset-block-start: 0; + inset-inline: 0; + } + + :host([orientation='vertical']) .thumb-container { + top: auto; + left: 50%; + transform: translate(-50%, 50%); + } + + ::slotted(*) { + color: ${bodyFontColor}; + font: ${bodyFont}; + } + } + + @layer hover { + :host(:not([disabled]):not([readonly]):hover) .thumb-container { + border-color: ${borderHoverColor}; + box-shadow: 0 0 0 ${borderWidth} ${borderHoverColor}; + } + } + + @layer focusVisible { + :host(:focus-visible) .thumb-container { + border-color: ${borderHoverColor}; + box-shadow: + 0 0 0 ${borderWidth} ${applicationBackgroundColor}, + 0 0 0 calc(${borderWidth} * 2) ${borderHoverColor}; + } + } + + @layer active { + :host(:not([disabled]):not([readonly]):active) .thumb-container { + border-color: ${borderHoverColor}; + background-color: ${fillDownColor}; + } + } + + @layer disabled { + :host([disabled]) { + color: ${bodyDisabledFontColor}; + cursor: default; + } + + :host([disabled]) .track { + background-color: rgba(${borderRgbPartialColor}, 0.1); + } + + :host([disabled]) .track-start { + background-color: rgba(${borderRgbPartialColor}, 0.2); + } + + :host([disabled]) .thumb-container { + border-color: rgba(${borderRgbPartialColor}, 0.2); + background-color: ${applicationBackgroundColor}; + box-shadow: none; + } + + :host([disabled]) ::slotted(*) { + color: ${bodyDisabledFontColor}; + } + } + + @layer top {} +`; diff --git a/packages/ok-components/src/basic/slider/tests/slider.spec.ts b/packages/ok-components/src/basic/slider/tests/slider.spec.ts new file mode 100644 index 0000000000..c367c78f43 --- /dev/null +++ b/packages/ok-components/src/basic/slider/tests/slider.spec.ts @@ -0,0 +1,22 @@ +import { html } from '@ni/fast-element'; +import { Slider, sliderTag } from '..'; +import { fixture } from '../../../utilities/tests/fixture'; + +describe('Slider', () => { + it('can construct an element instance', () => { + expect(document.createElement(sliderTag)).toBeInstanceOf(Slider); + }); + + it('provides the FAST Foundation slider template', async () => { + const { element, connect, disconnect } = await fixture( + html`<${sliderTag}>` + ); + await connect(); + + expect(element.shadowRoot?.querySelector('[part="track-container"]')).not.toBeNull(); + expect(element.shadowRoot?.querySelector('[part="track-start"]')).not.toBeNull(); + expect(element.shadowRoot?.querySelector('[part="thumb-container"]')).not.toBeNull(); + + await disconnect(); + }); +}); diff --git a/packages/storybook/src/ok/basic/slider/slider-matrix.stories.ts b/packages/storybook/src/ok/basic/slider/slider-matrix.stories.ts new file mode 100644 index 0000000000..3bcc580525 --- /dev/null +++ b/packages/storybook/src/ok/basic/slider/slider-matrix.stories.ts @@ -0,0 +1,60 @@ +import type { Meta, StoryFn } from '@storybook/html-vite'; +import { html, type ViewTemplate } from '@ni/fast-element'; +import { sliderTag } from '@ni/ok-components/dist/esm/basic/slider'; +import { + cartesianProduct, + createMatrix, + createMatrixInteractionsFromStates, + createMatrixThemeStory, + sharedMatrixParameters +} from '../../../utilities/matrix'; + +const metadata: Meta = { + title: 'Tests Ok/Basic Slider', + parameters: { + ...sharedMatrixParameters() + } +}; + +export default metadata; + +type SliderState = readonly [string, boolean, boolean]; + +const states: SliderState[] = [ + ['Enabled', false, false], + ['Readonly', false, true], + ['Disabled', true, false] +]; + +const interactionStates = cartesianProduct([states] as const); + +const component = ([name, disabled, readOnly]: SliderState): ViewTemplate => html` +
+ ${() => name} + <${sliderTag} + value="40" + ?disabled="${() => disabled}" + ?readonly="${() => readOnly}" + > +
+`; + +export const statesThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrix(component, [states]) +); + +export const orientationsThemeMatrix: StoryFn = createMatrixThemeStory(html` +
+ <${sliderTag} value="40"> + <${sliderTag} value="40" orientation="vertical"> +
+`); + +export const interactionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates(component, { + hover: interactionStates, + hoverActive: interactionStates, + active: interactionStates, + focus: interactionStates + }) +); diff --git a/packages/storybook/src/ok/basic/slider/slider.mdx b/packages/storybook/src/ok/basic/slider/slider.mdx new file mode 100644 index 0000000000..3ed5915b43 --- /dev/null +++ b/packages/storybook/src/ok/basic/slider/slider.mdx @@ -0,0 +1,16 @@ +import { Canvas, Controls, Meta, Title } from '@storybook/addon-docs/blocks'; +import ComponentApisLink from '../../../docs/component-apis-link.mdx'; +import * as sliderStories from './slider.stories'; + + + + +A Nimble-styled slider based on the FAST Foundation slider. + +<Canvas of={sliderStories.slider} /> + +## API + +<Controls of={sliderStories.slider} /> + +<ComponentApisLink /> diff --git a/packages/storybook/src/ok/basic/slider/slider.stories.ts b/packages/storybook/src/ok/basic/slider/slider.stories.ts new file mode 100644 index 0000000000..a9e00f98e9 --- /dev/null +++ b/packages/storybook/src/ok/basic/slider/slider.stories.ts @@ -0,0 +1,94 @@ +import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html-vite'; +import { html } from '@ni/fast-element'; +import { sliderTag } from '@ni/ok-components/dist/esm/basic/slider'; +import { withActions } from 'storybook/actions/decorator'; +import { + apiCategory, + createUserSelectedThemeStory, + disabledDescription, + okWarning +} from '../../../utilities/storybook'; + +interface SliderArgs { + value: number; + min: number; + max: number; + step: number; + orientation: 'horizontal' | 'vertical'; + disabled: boolean; + readOnly: boolean; + change?: (event: Event) => void; +} + +const metadata: Meta<SliderArgs> = { + title: 'Ok/Basic Slider', + decorators: [withActions<HtmlRenderer>], + parameters: { + actions: { + handles: ['change'] + } + }, + render: createUserSelectedThemeStory(html<SliderArgs>` + ${okWarning({ + componentName: 'Basic Slider', + statusLink: './?path=/docs/component-status--docs#ok-components' + })} + <${sliderTag} + value="${x => x.value}" + min="${x => x.min}" + max="${x => x.max}" + step="${x => x.step}" + orientation="${x => x.orientation}" + ?disabled="${x => x.disabled}" + ?readonly="${x => x.readOnly}" + ></${sliderTag}> + `), + argTypes: { + value: { + control: { type: 'number' }, + table: { category: apiCategory.attributes } + }, + min: { + control: { type: 'number' }, + table: { category: apiCategory.attributes } + }, + max: { + control: { type: 'number' }, + table: { category: apiCategory.attributes } + }, + step: { + control: { type: 'number' }, + table: { category: apiCategory.attributes } + }, + orientation: { + options: ['horizontal', 'vertical'], + control: { type: 'radio' }, + table: { category: apiCategory.attributes } + }, + disabled: { + description: disabledDescription({ componentName: 'slider' }), + table: { category: apiCategory.attributes } + }, + readOnly: { + name: 'readonly', + table: { category: apiCategory.attributes } + }, + change: { + table: { category: apiCategory.events }, + control: false + } + }, + args: { + value: 50, + min: 0, + max: 100, + step: 1, + orientation: 'horizontal', + disabled: false, + readOnly: false + } +}; + +export default metadata; + +export const slider: StoryObj<SliderArgs> = {}; From b0cb180b57e3931415862db093bd5b7c7e9e6693 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:46:23 +0000 Subject: [PATCH 02/26] Add OK React slider wrapper Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- ...t-7f04b43d-439e-4848-b394-511e0909be4d.json | 7 +++++++ .../ok-react/src/basic/slider/index.ts | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 change/@ni-ok-react-7f04b43d-439e-4848-b394-511e0909be4d.json create mode 100644 packages/react-workspace/ok-react/src/basic/slider/index.ts diff --git a/change/@ni-ok-react-7f04b43d-439e-4848-b394-511e0909be4d.json b/change/@ni-ok-react-7f04b43d-439e-4848-b394-511e0909be4d.json new file mode 100644 index 0000000000..3c3f861b97 --- /dev/null +++ b/change/@ni-ok-react-7f04b43d-439e-4848-b394-511e0909be4d.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add React wrapper for the basic slider", + "packageName": "@ni/ok-react", + "email": "198982749+Copilot@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-workspace/ok-react/src/basic/slider/index.ts b/packages/react-workspace/ok-react/src/basic/slider/index.ts new file mode 100644 index 0000000000..4d84c06ec6 --- /dev/null +++ b/packages/react-workspace/ok-react/src/basic/slider/index.ts @@ -0,0 +1,18 @@ +'use client'; + +import { + Slider, + sliderTag +} from '@ni/ok-components/dist/esm/basic/slider'; +import { wrap, type EventName } from '../../utilities/react-wrapper'; + +export { sliderTag }; +export { type Slider }; +export const OkBasicSlider = wrap(Slider, { + events: { + onChange: 'change' as EventName<SliderChangeEvent> + } +}); +export interface SliderChangeEvent extends CustomEvent { + target: Slider; +} From dbbf9a5b83e0b8b5f44f75b4cf6f82e342fc18be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:07:46 +0000 Subject: [PATCH 03/26] Scope slider to TestHub namespace Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- .github/CODEOWNERS | 2 ++ ...-ok-react-7f04b43d-439e-4848-b394-511e0909be4d.json | 2 +- packages/ok-components/src/all-components.ts | 2 +- .../src/{basic/all-basic.ts => th/all-th.ts} | 0 .../ok-components/src/{basic => th}/slider/index.ts | 10 +++++----- .../ok-components/src/{basic => th}/slider/styles.ts | 0 .../src/{basic => th}/slider/tests/slider.spec.ts | 0 .../ok-react/src/{basic => th}/slider/index.ts | 4 ++-- .../ok/{basic => th}/slider/slider-matrix.stories.ts | 4 ++-- .../storybook/src/ok/{basic => th}/slider/slider.mdx | 2 +- .../src/ok/{basic => th}/slider/slider.stories.ts | 6 +++--- 11 files changed, 17 insertions(+), 15 deletions(-) rename packages/ok-components/src/{basic/all-basic.ts => th/all-th.ts} (100%) rename packages/ok-components/src/{basic => th}/slider/index.ts (63%) rename packages/ok-components/src/{basic => th}/slider/styles.ts (100%) rename packages/ok-components/src/{basic => th}/slider/tests/slider.spec.ts (100%) rename packages/react-workspace/ok-react/src/{basic => th}/slider/index.ts (77%) rename packages/storybook/src/ok/{basic => th}/slider/slider-matrix.stories.ts (94%) rename packages/storybook/src/ok/{basic => th}/slider/slider.mdx (84%) rename packages/storybook/src/ok/{basic => th}/slider/slider.stories.ts (94%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6db7b95f2c..b14fa67450 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -22,6 +22,8 @@ /**/Ts/** @jattasNI /**/fv/** @jattasNI @fredvisser /**/Fv/** @jattasNI @fredvisser +/**/th/** @atmgrifter00 +/**/Th/** @atmgrifter00 /packages/nimble-tokens @rajsite @jattasNI @fredvisser /packages/storybook/.storybook @rajsite @jattasNI @fredvisser diff --git a/change/@ni-ok-react-7f04b43d-439e-4848-b394-511e0909be4d.json b/change/@ni-ok-react-7f04b43d-439e-4848-b394-511e0909be4d.json index 3c3f861b97..7f489ae30f 100644 --- a/change/@ni-ok-react-7f04b43d-439e-4848-b394-511e0909be4d.json +++ b/change/@ni-ok-react-7f04b43d-439e-4848-b394-511e0909be4d.json @@ -1,6 +1,6 @@ { "type": "minor", - "comment": "Add React wrapper for the basic slider", + "comment": "Add React wrapper for the TestHub slider", "packageName": "@ni/ok-react", "email": "198982749+Copilot@users.noreply.github.com", "dependentChangeType": "patch" diff --git a/packages/ok-components/src/all-components.ts b/packages/ok-components/src/all-components.ts index 526d6043fb..cf4f0c5784 100644 --- a/packages/ok-components/src/all-components.ts +++ b/packages/ok-components/src/all-components.ts @@ -6,7 +6,7 @@ import '@ni/spright-components/dist/esm/all-components'; -import './basic/all-basic'; +import './th/all-th'; import './fv/all-fv'; import './ex/all-ex'; import './ts/all-ts'; diff --git a/packages/ok-components/src/basic/all-basic.ts b/packages/ok-components/src/th/all-th.ts similarity index 100% rename from packages/ok-components/src/basic/all-basic.ts rename to packages/ok-components/src/th/all-th.ts diff --git a/packages/ok-components/src/basic/slider/index.ts b/packages/ok-components/src/th/slider/index.ts similarity index 63% rename from packages/ok-components/src/basic/slider/index.ts rename to packages/ok-components/src/th/slider/index.ts index a9b091128d..dae6c1ed57 100644 --- a/packages/ok-components/src/basic/slider/index.ts +++ b/packages/ok-components/src/th/slider/index.ts @@ -8,7 +8,7 @@ import { styles } from './styles'; declare global { interface HTMLElementTagNameMap { - 'ok-basic-slider': Slider; + 'ok-th-slider': Slider; } } @@ -17,12 +17,12 @@ declare global { */ export class Slider extends FoundationSlider {} -const okBasicSlider = Slider.compose<SliderOptions>({ - baseName: 'basic-slider', +const okThSlider = Slider.compose<SliderOptions>({ + baseName: 'th-slider', baseClass: FoundationSlider, template, styles }); -DesignSystem.getOrCreate().withPrefix('ok').register(okBasicSlider()); -export const sliderTag = 'ok-basic-slider'; +DesignSystem.getOrCreate().withPrefix('ok').register(okThSlider()); +export const sliderTag = 'ok-th-slider'; diff --git a/packages/ok-components/src/basic/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts similarity index 100% rename from packages/ok-components/src/basic/slider/styles.ts rename to packages/ok-components/src/th/slider/styles.ts diff --git a/packages/ok-components/src/basic/slider/tests/slider.spec.ts b/packages/ok-components/src/th/slider/tests/slider.spec.ts similarity index 100% rename from packages/ok-components/src/basic/slider/tests/slider.spec.ts rename to packages/ok-components/src/th/slider/tests/slider.spec.ts diff --git a/packages/react-workspace/ok-react/src/basic/slider/index.ts b/packages/react-workspace/ok-react/src/th/slider/index.ts similarity index 77% rename from packages/react-workspace/ok-react/src/basic/slider/index.ts rename to packages/react-workspace/ok-react/src/th/slider/index.ts index 4d84c06ec6..97739c4064 100644 --- a/packages/react-workspace/ok-react/src/basic/slider/index.ts +++ b/packages/react-workspace/ok-react/src/th/slider/index.ts @@ -3,12 +3,12 @@ import { Slider, sliderTag -} from '@ni/ok-components/dist/esm/basic/slider'; +} from '@ni/ok-components/dist/esm/th/slider'; import { wrap, type EventName } from '../../utilities/react-wrapper'; export { sliderTag }; export { type Slider }; -export const OkBasicSlider = wrap(Slider, { +export const OkThSlider = wrap(Slider, { events: { onChange: 'change' as EventName<SliderChangeEvent> } diff --git a/packages/storybook/src/ok/basic/slider/slider-matrix.stories.ts b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts similarity index 94% rename from packages/storybook/src/ok/basic/slider/slider-matrix.stories.ts rename to packages/storybook/src/ok/th/slider/slider-matrix.stories.ts index 3bcc580525..03504d1a86 100644 --- a/packages/storybook/src/ok/basic/slider/slider-matrix.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/html-vite'; import { html, type ViewTemplate } from '@ni/fast-element'; -import { sliderTag } from '@ni/ok-components/dist/esm/basic/slider'; +import { sliderTag } from '@ni/ok-components/dist/esm/th/slider'; import { cartesianProduct, createMatrix, @@ -10,7 +10,7 @@ import { } from '../../../utilities/matrix'; const metadata: Meta = { - title: 'Tests Ok/Basic Slider', + title: 'Tests Ok/Th Slider', parameters: { ...sharedMatrixParameters() } diff --git a/packages/storybook/src/ok/basic/slider/slider.mdx b/packages/storybook/src/ok/th/slider/slider.mdx similarity index 84% rename from packages/storybook/src/ok/basic/slider/slider.mdx rename to packages/storybook/src/ok/th/slider/slider.mdx index 3ed5915b43..37ae8629fb 100644 --- a/packages/storybook/src/ok/basic/slider/slider.mdx +++ b/packages/storybook/src/ok/th/slider/slider.mdx @@ -5,7 +5,7 @@ import * as sliderStories from './slider.stories'; <Meta of={sliderStories} /> <Title of={sliderStories} /> -A Nimble-styled slider based on the FAST Foundation slider. +A Nimble-styled TestHub slider based on the FAST Foundation slider. <Canvas of={sliderStories.slider} /> diff --git a/packages/storybook/src/ok/basic/slider/slider.stories.ts b/packages/storybook/src/ok/th/slider/slider.stories.ts similarity index 94% rename from packages/storybook/src/ok/basic/slider/slider.stories.ts rename to packages/storybook/src/ok/th/slider/slider.stories.ts index a9e00f98e9..0decc21265 100644 --- a/packages/storybook/src/ok/basic/slider/slider.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider.stories.ts @@ -1,6 +1,6 @@ import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html-vite'; import { html } from '@ni/fast-element'; -import { sliderTag } from '@ni/ok-components/dist/esm/basic/slider'; +import { sliderTag } from '@ni/ok-components/dist/esm/th/slider'; import { withActions } from 'storybook/actions/decorator'; import { apiCategory, @@ -21,7 +21,7 @@ interface SliderArgs { } const metadata: Meta<SliderArgs> = { - title: 'Ok/Basic Slider', + title: 'Ok/Th Slider', decorators: [withActions<HtmlRenderer>], parameters: { actions: { @@ -30,7 +30,7 @@ const metadata: Meta<SliderArgs> = { }, render: createUserSelectedThemeStory(html<SliderArgs>` ${okWarning({ - componentName: 'Basic Slider', + componentName: 'Th Slider', statusLink: './?path=/docs/component-status--docs#ok-components' })} <${sliderTag} From 35777765acfb68f50ed30eb7e20667432119176d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:17:01 +0000 Subject: [PATCH 04/26] Align slider visuals with switch Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- ...-235da122-2e5b-42cd-a1f9-52751ccbff2b.json | 7 + package-lock.json | 1 + packages/ok-components/package.json | 1 + .../ok-components/src/th/slider/styles.ts | 214 +++++++++++++++--- 4 files changed, 191 insertions(+), 32 deletions(-) create mode 100644 change/@ni-ok-components-235da122-2e5b-42cd-a1f9-52751ccbff2b.json diff --git a/change/@ni-ok-components-235da122-2e5b-42cd-a1f9-52751ccbff2b.json b/change/@ni-ok-components-235da122-2e5b-42cd-a1f9-52751ccbff2b.json new file mode 100644 index 0000000000..9ccd08540c --- /dev/null +++ b/change/@ni-ok-components-235da122-2e5b-42cd-a1f9-52751ccbff2b.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add the TestHub slider component", + "packageName": "@ni/ok-components", + "email": "198982749+Copilot@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/package-lock.json b/package-lock.json index 7ceaa95fa3..4a01a1f802 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33559,6 +33559,7 @@ "@ni/fast-foundation": "^10.2.5", "@ni/fast-web-utilities": "^10.0.0", "@ni/nimble-components": "^35.13.0", + "@ni/nimble-tokens": "^8.19.0", "@ni/spright-components": "^6.22.0", "tslib": "^2.2.0" }, diff --git a/packages/ok-components/package.json b/packages/ok-components/package.json index 0f8e1e7b41..5f9bd26f28 100644 --- a/packages/ok-components/package.json +++ b/packages/ok-components/package.json @@ -44,6 +44,7 @@ "@ni/fast-foundation": "^10.2.5", "@ni/fast-web-utilities": "^10.0.0", "@ni/nimble-components": "^35.13.0", + "@ni/nimble-tokens": "^8.19.0", "@ni/spright-components": "^6.22.0", "tslib": "^2.2.0" }, diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 5aba858a29..bc1bfb491f 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -1,17 +1,24 @@ import { css } from '@ni/fast-element'; import { - applicationBackgroundColor, bodyDisabledFontColor, bodyFont, bodyFontColor, - borderColor, borderHoverColor, - borderRgbPartialColor, borderWidth, - fillDownColor, - fillSelectedColor, smallDelay } from '@ni/nimble-components/dist/esm/theme-provider/design-tokens'; +import { + Black15, + Black75, + Black91, + DigitalGreenLight, + PowerGreen, + White +} from '@ni/nimble-tokens/dist/styledictionary/js/tokens'; +import { Theme } from '@ni/nimble-components/dist/esm/theme-provider/types'; +import { hexToRgbaCssColor } from '@ni/nimble-components/dist/esm/utilities/style/colors'; +import { focusVisible } from '@ni/nimble-components/dist/esm/utilities/style/focus'; +import { themeBehavior } from '@ni/nimble-components/dist/esm/utilities/style/theme'; import { display } from '../../utilities/style/display'; export const styles = css` @@ -53,7 +60,7 @@ export const styles = css` width: 100%; height: 4px; border-radius: 2px; - background-color: rgba(${borderRgbPartialColor}, 0.3); + background-color: var(--ni-private-slider-track-background-color); } .track-start { @@ -61,23 +68,35 @@ export const styles = css` inset-block: 0; left: 0; border-radius: inherit; - background-color: ${fillSelectedColor}; + background-color: var( + --ni-private-slider-track-background-selected-color + ); } .thumb-container { position: absolute; top: 50%; - width: 16px; - height: 16px; - box-sizing: border-box; - border: ${borderWidth} solid ${borderColor}; + display: flex; + width: 24px; + height: 24px; + align-items: center; + justify-content: center; + border: ${borderWidth} solid + var(--ni-private-slider-thumb-border-selected-color); border-radius: 50%; - background-color: ${applicationBackgroundColor}; + background-color: var( + --ni-private-slider-thumb-background-color + ); transform: translate(50%, -50%); - transition: - border-color ${smallDelay} ease-in-out, - box-shadow ${smallDelay} ease-in-out, - background-color ${smallDelay} ease-in-out; + transition: border-color ${smallDelay} ease-in-out; + } + + .thumb-container::after { + content: ''; + width: 18px; + height: 18px; + border-radius: 50%; + opacity: 0; } :host([orientation='vertical']) .positioning-region { @@ -108,24 +127,44 @@ export const styles = css` @layer hover { :host(:not([disabled]):not([readonly]):hover) .thumb-container { - border-color: ${borderHoverColor}; - box-shadow: 0 0 0 ${borderWidth} ${borderHoverColor}; + border: calc(${borderWidth} * 2) solid ${borderHoverColor}; + } + + :host(:not([disabled]):not([readonly]):hover) .track-start { + background-color: var( + --ni-private-slider-track-background-selected-rollover-color + ); } } @layer focusVisible { - :host(:focus-visible) .thumb-container { - border-color: ${borderHoverColor}; - box-shadow: - 0 0 0 ${borderWidth} ${applicationBackgroundColor}, - 0 0 0 calc(${borderWidth} * 2) ${borderHoverColor}; + :host(${focusVisible}) .thumb-container { + border: calc(${borderWidth} * 2) solid ${borderHoverColor}; + } + + :host(${focusVisible}) .thumb-container::after { + border: ${borderWidth} solid + var(--ni-private-slider-thumb-border-selected-color); + background-color: var( + --ni-private-slider-thumb-background-color + ); + opacity: 1; + } + + :host(${focusVisible}) .track-start { + background-color: var( + --ni-private-slider-track-background-selected-rollover-color + ); } } @layer active { - :host(:not([disabled]):not([readonly]):active) .thumb-container { - border-color: ${borderHoverColor}; - background-color: ${fillDownColor}; + :host(:not([disabled]):not([readonly]):active) + .thumb-container::after { + background-color: var( + --ni-private-slider-thumb-background-active-color + ); + opacity: 1; } } @@ -136,17 +175,25 @@ export const styles = css` } :host([disabled]) .track { - background-color: rgba(${borderRgbPartialColor}, 0.1); + background-color: var( + --ni-private-slider-track-background-disabled-color + ); } :host([disabled]) .track-start { - background-color: rgba(${borderRgbPartialColor}, 0.2); + background-color: var( + --ni-private-slider-track-background-disabled-color + ); } :host([disabled]) .thumb-container { - border-color: rgba(${borderRgbPartialColor}, 0.2); - background-color: ${applicationBackgroundColor}; - box-shadow: none; + border: ${borderWidth} solid + var(--ni-private-slider-thumb-border-disabled-color); + background-color: transparent; + } + + :host([disabled]) .thumb-container::after { + opacity: 0; } :host([disabled]) ::slotted(*) { @@ -155,4 +202,107 @@ export const styles = css` } @layer top {} -`; +`.withBehaviors( + themeBehavior( + Theme.light, + css` + :host { + --ni-private-slider-track-background-color: ${hexToRgbaCssColor( + Black91, + 0.1 + )}; + --ni-private-slider-track-background-selected-color: ${hexToRgbaCssColor( + DigitalGreenLight, + 0.6 + )}; + --ni-private-slider-track-background-selected-rollover-color: ${hexToRgbaCssColor( + DigitalGreenLight, + 0.3 + )}; + --ni-private-slider-track-background-disabled-color: ${hexToRgbaCssColor( + Black91, + 0.07 + )}; + --ni-private-slider-thumb-background-color: ${White}; + --ni-private-slider-thumb-background-active-color: ${hexToRgbaCssColor( + DigitalGreenLight, + 0.3 + )}; + --ni-private-slider-thumb-border-selected-color: ${DigitalGreenLight}; + --ni-private-slider-thumb-border-disabled-color: ${hexToRgbaCssColor( + Black91, + 0.15 + )}; + } + ` + ), + themeBehavior( + Theme.dark, + css` + :host { + --ni-private-slider-track-background-color: ${hexToRgbaCssColor( + Black15, + 0.1 + )}; + --ni-private-slider-track-background-selected-color: ${hexToRgbaCssColor( + PowerGreen, + 0.6 + )}; + --ni-private-slider-track-background-selected-rollover-color: ${hexToRgbaCssColor( + PowerGreen, + 0.3 + )}; + --ni-private-slider-track-background-disabled-color: ${hexToRgbaCssColor( + Black15, + 0.07 + )}; + --ni-private-slider-thumb-background-color: ${Black75}; + --ni-private-slider-thumb-background-active-color: ${hexToRgbaCssColor( + PowerGreen, + 0.3 + )}; + --ni-private-slider-thumb-border-selected-color: ${PowerGreen}; + --ni-private-slider-thumb-border-disabled-color: ${hexToRgbaCssColor( + Black15, + 0.15 + )}; + } + ` + ), + themeBehavior( + Theme.color, + css` + :host { + --ni-private-slider-track-background-color: ${hexToRgbaCssColor( + White, + 0.1 + )}; + --ni-private-slider-track-background-selected-color: ${hexToRgbaCssColor( + White, + 0.6 + )}; + --ni-private-slider-track-background-selected-rollover-color: ${hexToRgbaCssColor( + White, + 0.3 + )}; + --ni-private-slider-track-background-disabled-color: ${hexToRgbaCssColor( + White, + 0.07 + )}; + --ni-private-slider-thumb-background-color: ${hexToRgbaCssColor( + White, + 0.3 + )}; + --ni-private-slider-thumb-background-active-color: ${hexToRgbaCssColor( + White, + 0.3 + )}; + --ni-private-slider-thumb-border-selected-color: ${White}; + --ni-private-slider-thumb-border-disabled-color: ${hexToRgbaCssColor( + White, + 0.3 + )}; + } + ` + ) +); From ae9ad82d92fd5b059b0ba86d9edb8570565f6ff3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:26:58 +0000 Subject: [PATCH 05/26] Add slider value feedback Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/index.ts | 35 ++++++++- .../ok-components/src/th/slider/styles.ts | 73 +++++++++---------- .../src/th/slider/tests/slider.spec.ts | 20 +++++ .../src/ok/th/slider/slider-matrix.stories.ts | 5 +- .../storybook/src/ok/th/slider/slider.mdx | 2 + .../src/ok/th/slider/slider.stories.ts | 10 ++- 6 files changed, 100 insertions(+), 45 deletions(-) diff --git a/packages/ok-components/src/th/slider/index.ts b/packages/ok-components/src/th/slider/index.ts index dae6c1ed57..8136348e0b 100644 --- a/packages/ok-components/src/th/slider/index.ts +++ b/packages/ok-components/src/th/slider/index.ts @@ -1,3 +1,4 @@ +import { attr, html, ref } from '@ni/fast-element'; import { DesignSystem, Slider as FoundationSlider, @@ -15,13 +16,43 @@ declare global { /** * A Nimble-styled slider control. */ -export class Slider extends FoundationSlider {} +export class Slider extends FoundationSlider { + /** + * Whether the current value is displayed next to the thumb. + */ + @attr({ attribute: 'value-visible', mode: 'boolean' }) + public valueVisible = false; + + /** @internal */ + public valueLabel?: HTMLSpanElement; + + /** @internal */ + public override connectedCallback(): void { + super.connectedCallback(); + this.updateValueLabel(); + } + + /** @internal */ + public override valueChanged(previous: string, next: string): void { + super.valueChanged(previous, next); + this.updateValueLabel(); + } + + private updateValueLabel(): void { + if (this.valueLabel) { + this.valueLabel.textContent = this.value; + } + } +} const okThSlider = Slider.compose<SliderOptions>({ baseName: 'th-slider', baseClass: FoundationSlider, template, - styles + styles, + thumb: html<Slider>` + <span ${ref('valueLabel')} class="value-label" aria-hidden="true"></span> + ` }); DesignSystem.getOrCreate().withPrefix('ok').register(okThSlider()); diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index bc1bfb491f..56a755f9e9 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -1,11 +1,18 @@ import { css } from '@ni/fast-element'; import { + applicationBackgroundColor, bodyDisabledFontColor, bodyFont, bodyFontColor, borderHoverColor, borderWidth, - smallDelay + elevation2BoxShadow, + fillSelectedColor, + popupBorderColor, + smallDelay, + smallPadding, + tooltipCaptionFont, + tooltipCaptionFontColor } from '@ni/nimble-components/dist/esm/theme-provider/design-tokens'; import { Black15, @@ -68,9 +75,7 @@ export const styles = css` inset-block: 0; left: 0; border-radius: inherit; - background-color: var( - --ni-private-slider-track-background-selected-color - ); + background-color: ${fillSelectedColor}; } .thumb-container { @@ -99,6 +104,25 @@ export const styles = css` opacity: 0; } + .value-label { + position: absolute; + inset-block-end: calc(100% + ${smallPadding}); + display: none; + padding: ${smallPadding}; + border: ${borderWidth} solid ${popupBorderColor}; + background-color: ${applicationBackgroundColor}; + box-shadow: ${elevation2BoxShadow}; + color: ${tooltipCaptionFontColor}; + font: ${tooltipCaptionFont}; + line-height: 1; + white-space: nowrap; + pointer-events: none; + } + + :host([value-visible]) .value-label { + display: block; + } + :host([orientation='vertical']) .positioning-region { align-items: initial; } @@ -119,6 +143,11 @@ export const styles = css` transform: translate(-50%, 50%); } + :host([orientation='vertical']) .value-label { + inset-block-end: auto; + inset-inline-start: calc(100% + ${smallPadding}); + } + ::slotted(*) { color: ${bodyFontColor}; font: ${bodyFont}; @@ -129,12 +158,6 @@ export const styles = css` :host(:not([disabled]):not([readonly]):hover) .thumb-container { border: calc(${borderWidth} * 2) solid ${borderHoverColor}; } - - :host(:not([disabled]):not([readonly]):hover) .track-start { - background-color: var( - --ni-private-slider-track-background-selected-rollover-color - ); - } } @layer focusVisible { @@ -150,12 +173,6 @@ export const styles = css` ); opacity: 1; } - - :host(${focusVisible}) .track-start { - background-color: var( - --ni-private-slider-track-background-selected-rollover-color - ); - } } @layer active { @@ -211,14 +228,6 @@ export const styles = css` Black91, 0.1 )}; - --ni-private-slider-track-background-selected-color: ${hexToRgbaCssColor( - DigitalGreenLight, - 0.6 - )}; - --ni-private-slider-track-background-selected-rollover-color: ${hexToRgbaCssColor( - DigitalGreenLight, - 0.3 - )}; --ni-private-slider-track-background-disabled-color: ${hexToRgbaCssColor( Black91, 0.07 @@ -244,14 +253,6 @@ export const styles = css` Black15, 0.1 )}; - --ni-private-slider-track-background-selected-color: ${hexToRgbaCssColor( - PowerGreen, - 0.6 - )}; - --ni-private-slider-track-background-selected-rollover-color: ${hexToRgbaCssColor( - PowerGreen, - 0.3 - )}; --ni-private-slider-track-background-disabled-color: ${hexToRgbaCssColor( Black15, 0.07 @@ -277,14 +278,6 @@ export const styles = css` White, 0.1 )}; - --ni-private-slider-track-background-selected-color: ${hexToRgbaCssColor( - White, - 0.6 - )}; - --ni-private-slider-track-background-selected-rollover-color: ${hexToRgbaCssColor( - White, - 0.3 - )}; --ni-private-slider-track-background-disabled-color: ${hexToRgbaCssColor( White, 0.07 diff --git a/packages/ok-components/src/th/slider/tests/slider.spec.ts b/packages/ok-components/src/th/slider/tests/slider.spec.ts index c367c78f43..644bce2986 100644 --- a/packages/ok-components/src/th/slider/tests/slider.spec.ts +++ b/packages/ok-components/src/th/slider/tests/slider.spec.ts @@ -19,4 +19,24 @@ describe('Slider', () => { await disconnect(); }); + + it('can display the current value next to the thumb', async () => { + const { element, connect, disconnect } = await fixture<Slider>( + html`<${sliderTag} value="4" value-visible></${sliderTag}>` + ); + await connect(); + + expect(element.valueVisible).toBeTrue(); + expect( + element.shadowRoot?.querySelector('.value-label')?.textContent?.trim() + ).toBe('4'); + + element.value = '7'; + await Promise.resolve(); + expect( + element.shadowRoot?.querySelector('.value-label')?.textContent?.trim() + ).toBe('7'); + + await disconnect(); + }); }); diff --git a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts index 03504d1a86..20a39057dd 100644 --- a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts @@ -35,6 +35,7 @@ const component = ([name, disabled, readOnly]: SliderState): ViewTemplate => htm value="40" ?disabled="${() => disabled}" ?readonly="${() => readOnly}" + value-visible ></${sliderTag}> </div> `; @@ -45,8 +46,8 @@ export const statesThemeMatrix: StoryFn = createMatrixThemeStory( export const orientationsThemeMatrix: StoryFn = createMatrixThemeStory(html` <div style="display: flex; gap: 32px; align-items: flex-start; padding: 16px;"> - <${sliderTag} value="40"></${sliderTag}> - <${sliderTag} value="40" orientation="vertical"></${sliderTag}> + <${sliderTag} value="40" value-visible></${sliderTag}> + <${sliderTag} value="40" orientation="vertical" value-visible></${sliderTag}> </div> `); diff --git a/packages/storybook/src/ok/th/slider/slider.mdx b/packages/storybook/src/ok/th/slider/slider.mdx index 37ae8629fb..26f82d2743 100644 --- a/packages/storybook/src/ok/th/slider/slider.mdx +++ b/packages/storybook/src/ok/th/slider/slider.mdx @@ -6,6 +6,8 @@ import * as sliderStories from './slider.stories'; <Title of={sliderStories} /> A Nimble-styled TestHub slider based on the FAST Foundation slider. +Set `value-visible` to display a live value tooltip beside the thumb as it +moves. <Canvas of={sliderStories.slider} /> diff --git a/packages/storybook/src/ok/th/slider/slider.stories.ts b/packages/storybook/src/ok/th/slider/slider.stories.ts index 0decc21265..629a0e611d 100644 --- a/packages/storybook/src/ok/th/slider/slider.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider.stories.ts @@ -17,6 +17,7 @@ interface SliderArgs { orientation: 'horizontal' | 'vertical'; disabled: boolean; readOnly: boolean; + valueVisible: boolean; change?: (event: Event) => void; } @@ -41,6 +42,7 @@ const metadata: Meta<SliderArgs> = { orientation="${x => x.orientation}" ?disabled="${x => x.disabled}" ?readonly="${x => x.readOnly}" + ?value-visible="${x => x.valueVisible}" ></${sliderTag}> `), argTypes: { @@ -73,6 +75,11 @@ const metadata: Meta<SliderArgs> = { name: 'readonly', table: { category: apiCategory.attributes } }, + valueVisible: { + name: 'value-visible', + description: 'Displays the current value next to the slider thumb.', + table: { category: apiCategory.attributes } + }, change: { table: { category: apiCategory.events }, control: false @@ -85,7 +92,8 @@ const metadata: Meta<SliderArgs> = { step: 1, orientation: 'horizontal', disabled: false, - readOnly: false + readOnly: false, + valueVisible: true } }; From 8d2ec570653f7295e787e10b89d5ca4e9b034330 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:11:41 +0000 Subject: [PATCH 06/26] Refine slider thumb and labels Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/index.ts | 9 +-- .../ok-components/src/th/slider/styles.ts | 70 ++++++++++++------- .../ok-components/src/th/slider/template.ts | 38 ++++++++++ .../src/th/slider/tests/slider.spec.ts | 2 + 4 files changed, 86 insertions(+), 33 deletions(-) create mode 100644 packages/ok-components/src/th/slider/template.ts diff --git a/packages/ok-components/src/th/slider/index.ts b/packages/ok-components/src/th/slider/index.ts index 8136348e0b..72d4098ac8 100644 --- a/packages/ok-components/src/th/slider/index.ts +++ b/packages/ok-components/src/th/slider/index.ts @@ -1,11 +1,11 @@ -import { attr, html, ref } from '@ni/fast-element'; +import { attr } from '@ni/fast-element'; import { DesignSystem, Slider as FoundationSlider, - sliderTemplate as template, type SliderOptions } from '@ni/fast-foundation'; import { styles } from './styles'; +import { template } from './template'; declare global { interface HTMLElementTagNameMap { @@ -49,10 +49,7 @@ const okThSlider = Slider.compose<SliderOptions>({ baseName: 'th-slider', baseClass: FoundationSlider, template, - styles, - thumb: html<Slider>` - <span ${ref('valueLabel')} class="value-label" aria-hidden="true"></span> - ` + styles }); DesignSystem.getOrCreate().withPrefix('ok').register(okThSlider()); diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 56a755f9e9..787bd00e0f 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -6,17 +6,13 @@ import { bodyFontColor, borderHoverColor, borderWidth, - elevation2BoxShadow, fillSelectedColor, - popupBorderColor, smallDelay, smallPadding, - tooltipCaptionFont, - tooltipCaptionFontColor + tooltipCaptionFont } from '@ni/nimble-components/dist/esm/theme-provider/design-tokens'; import { Black15, - Black75, Black91, DigitalGreenLight, PowerGreen, @@ -82,24 +78,23 @@ export const styles = css` position: absolute; top: 50%; display: flex; - width: 24px; - height: 24px; + width: 14px; + height: 14px; + box-sizing: border-box; align-items: center; justify-content: center; border: ${borderWidth} solid var(--ni-private-slider-thumb-border-selected-color); border-radius: 50%; - background-color: var( - --ni-private-slider-thumb-background-color - ); + background-color: ${applicationBackgroundColor}; transform: translate(50%, -50%); transition: border-color ${smallDelay} ease-in-out; } .thumb-container::after { content: ''; - width: 18px; - height: 18px; + width: 10px; + height: 10px; border-radius: 50%; opacity: 0; } @@ -108,11 +103,6 @@ export const styles = css` position: absolute; inset-block-end: calc(100% + ${smallPadding}); display: none; - padding: ${smallPadding}; - border: ${borderWidth} solid ${popupBorderColor}; - background-color: ${applicationBackgroundColor}; - box-shadow: ${elevation2BoxShadow}; - color: ${tooltipCaptionFontColor}; font: ${tooltipCaptionFont}; line-height: 1; white-space: nowrap; @@ -123,6 +113,28 @@ export const styles = css` display: block; } + .range-label { + position: absolute; + inset-block-start: calc(50% + 8px); + display: none; + font: ${tooltipCaptionFont}; + line-height: 1; + white-space: nowrap; + pointer-events: none; + } + + .minimum-label { + inset-inline-start: 0; + } + + .maximum-label { + inset-inline-end: 0; + } + + :host(:hover) .range-label { + display: block; + } + :host([orientation='vertical']) .positioning-region { align-items: initial; } @@ -148,6 +160,18 @@ export const styles = css` inset-inline-start: calc(100% + ${smallPadding}); } + :host([orientation='vertical']) .range-label { + inset-inline-start: calc(50% + 8px); + } + + :host([orientation='vertical']) .minimum-label { + inset-block: auto 0; + } + + :host([orientation='vertical']) .maximum-label { + inset-block: 0 auto; + } + ::slotted(*) { color: ${bodyFontColor}; font: ${bodyFont}; @@ -168,9 +192,7 @@ export const styles = css` :host(${focusVisible}) .thumb-container::after { border: ${borderWidth} solid var(--ni-private-slider-thumb-border-selected-color); - background-color: var( - --ni-private-slider-thumb-background-color - ); + background-color: ${applicationBackgroundColor}; opacity: 1; } } @@ -206,7 +228,7 @@ export const styles = css` :host([disabled]) .thumb-container { border: ${borderWidth} solid var(--ni-private-slider-thumb-border-disabled-color); - background-color: transparent; + background-color: ${applicationBackgroundColor}; } :host([disabled]) .thumb-container::after { @@ -232,7 +254,6 @@ export const styles = css` Black91, 0.07 )}; - --ni-private-slider-thumb-background-color: ${White}; --ni-private-slider-thumb-background-active-color: ${hexToRgbaCssColor( DigitalGreenLight, 0.3 @@ -257,7 +278,6 @@ export const styles = css` Black15, 0.07 )}; - --ni-private-slider-thumb-background-color: ${Black75}; --ni-private-slider-thumb-background-active-color: ${hexToRgbaCssColor( PowerGreen, 0.3 @@ -282,10 +302,6 @@ export const styles = css` White, 0.07 )}; - --ni-private-slider-thumb-background-color: ${hexToRgbaCssColor( - White, - 0.3 - )}; --ni-private-slider-thumb-background-active-color: ${hexToRgbaCssColor( White, 0.3 diff --git a/packages/ok-components/src/th/slider/template.ts b/packages/ok-components/src/th/slider/template.ts new file mode 100644 index 0000000000..f6d2ee3073 --- /dev/null +++ b/packages/ok-components/src/th/slider/template.ts @@ -0,0 +1,38 @@ +import { html, ref } from '@ni/fast-element'; +import type { Slider } from '.'; + +export const template = html<Slider>` + <template + role="slider" + tabindex="${x => (x.disabled ? null : 0)}" + aria-valuetext="${x => x.valueTextFormatter(x.value)}" + aria-valuenow="${x => x.value}" + aria-valuemin="${x => x.min}" + aria-valuemax="${x => x.max}" + aria-disabled="${x => (x.disabled ? true : undefined)}" + aria-readonly="${x => (x.readOnly ? true : undefined)}" + aria-orientation="${x => x.orientation}" + class="${x => x.orientation}" + > + <div part="positioning-region" class="positioning-region"> + <div ${ref('track')} part="track-container" class="track"> + <slot name="track"></slot> + <div part="track-start" class="track-start" style="${x => x.position}"> + <slot name="track-start"></slot> + </div> + </div> + <span class="range-label minimum-label" aria-hidden="true">${x => x.min}</span> + <span class="range-label maximum-label" aria-hidden="true">${x => x.max}</span> + <slot></slot> + <div + ${ref('thumb')} + part="thumb-container" + class="thumb-container" + style="${x => x.position}" + > + <slot name="thumb"></slot> + <span ${ref('valueLabel')} class="value-label" aria-hidden="true"></span> + </div> + </div> + </template> +`; diff --git a/packages/ok-components/src/th/slider/tests/slider.spec.ts b/packages/ok-components/src/th/slider/tests/slider.spec.ts index 644bce2986..1f192821c4 100644 --- a/packages/ok-components/src/th/slider/tests/slider.spec.ts +++ b/packages/ok-components/src/th/slider/tests/slider.spec.ts @@ -16,6 +16,8 @@ describe('Slider', () => { expect(element.shadowRoot?.querySelector('[part="track-container"]')).not.toBeNull(); expect(element.shadowRoot?.querySelector('[part="track-start"]')).not.toBeNull(); expect(element.shadowRoot?.querySelector('[part="thumb-container"]')).not.toBeNull(); + expect(element.shadowRoot?.querySelector('.minimum-label')?.textContent?.trim()).toBe('0'); + expect(element.shadowRoot?.querySelector('.maximum-label')?.textContent?.trim()).toBe('10'); await disconnect(); }); From 26c2893d78c6abd8733d4b159a8278c81442c470 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:27:44 +0000 Subject: [PATCH 07/26] Keep slider value stable on hover Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/styles.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 787bd00e0f..1003dd5eb0 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -88,7 +88,9 @@ export const styles = css` border-radius: 50%; background-color: ${applicationBackgroundColor}; transform: translate(50%, -50%); - transition: border-color ${smallDelay} ease-in-out; + transition: + border-color ${smallDelay} ease-in-out, + box-shadow ${smallDelay} ease-in-out; } .thumb-container::after { @@ -180,13 +182,15 @@ export const styles = css` @layer hover { :host(:not([disabled]):not([readonly]):hover) .thumb-container { - border: calc(${borderWidth} * 2) solid ${borderHoverColor}; + border-color: ${borderHoverColor}; + box-shadow: inset 0 0 0 ${borderWidth} ${borderHoverColor}; } } @layer focusVisible { :host(${focusVisible}) .thumb-container { - border: calc(${borderWidth} * 2) solid ${borderHoverColor}; + border-color: ${borderHoverColor}; + box-shadow: inset 0 0 0 ${borderWidth} ${borderHoverColor}; } :host(${focusVisible}) .thumb-container::after { From 10dd851278cb7b7e95da9ad64e4aad2f3220e1f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:35:24 +0000 Subject: [PATCH 08/26] Make slider width responsive Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/styles.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 1003dd5eb0..6d7a263e8d 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -31,7 +31,8 @@ export const styles = css` ${display('inline-block')} :host { - width: 240px; + width: 100%; + min-width: 42px; height: 32px; outline: none; color: ${bodyFontColor}; @@ -42,6 +43,7 @@ export const styles = css` :host([orientation='vertical']) { width: 32px; + min-width: 32px; height: 240px; } From 971f1dca1d8530d0ae88d66539e8b3c45e8a7411 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:40:01 +0000 Subject: [PATCH 09/26] Make vertical slider height responsive Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/styles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 6d7a263e8d..9be1503dbc 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -44,7 +44,8 @@ export const styles = css` :host([orientation='vertical']) { width: 32px; min-width: 32px; - height: 240px; + height: 100%; + min-height: 42px; } :host([readonly]) { From aa40e118ad65273c2612c440083b7fa2ad08080c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:40:42 +0000 Subject: [PATCH 10/26] Document slider minimum length Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/styles.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 9be1503dbc..e8836034fd 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -32,6 +32,7 @@ export const styles = css` :host { width: 100%; + /* Three thumb widths allow three non-overlapping slider positions. */ min-width: 42px; height: 32px; outline: none; From 0d040d080f855f4ae1c07542a8da566ca388aaf6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:45:17 +0000 Subject: [PATCH 11/26] Match slider fill to pressed thumb Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/styles.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index e8836034fd..bad717665b 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -6,7 +6,6 @@ import { bodyFontColor, borderHoverColor, borderWidth, - fillSelectedColor, smallDelay, smallPadding, tooltipCaptionFont @@ -75,7 +74,9 @@ export const styles = css` inset-block: 0; left: 0; border-radius: inherit; - background-color: ${fillSelectedColor}; + background-color: var( + --ni-private-slider-thumb-background-active-color + ); } .thumb-container { From 63b762303b0f394c452e8bc80036c24434b92940 Mon Sep 17 00:00:00 2001 From: Jonathan Meyer <jonathan.meyer@emerson.com> Date: Sat, 29 Aug 2026 15:57:16 -0500 Subject: [PATCH 12/26] Minor fixes. --- packages/ok-components/src/th/slider/styles.ts | 5 +---- packages/storybook/src/ok/th/slider/slider-matrix.stories.ts | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index bad717665b..6730a85547 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -75,7 +75,7 @@ export const styles = css` left: 0; border-radius: inherit; background-color: var( - --ni-private-slider-thumb-background-active-color + --ni-private-slider-thumb-border-selected-color ); } @@ -93,9 +93,6 @@ export const styles = css` border-radius: 50%; background-color: ${applicationBackgroundColor}; transform: translate(50%, -50%); - transition: - border-color ${smallDelay} ease-in-out, - box-shadow ${smallDelay} ease-in-out; } .thumb-container::after { diff --git a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts index 20a39057dd..2b60a0bb7a 100644 --- a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts @@ -32,6 +32,7 @@ const component = ([name, disabled, readOnly]: SliderState): ViewTemplate => htm <div style="display: inline-flex; flex-direction: column; gap: 8px; margin: 8px;"> <span>${() => name}</span> <${sliderTag} + style="width: 200px;" value="40" ?disabled="${() => disabled}" ?readonly="${() => readOnly}" From 6176c123ff13ef298ff1aecd7417b384938fd474 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:28:31 +0000 Subject: [PATCH 13/26] Remove unused slider token import Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/styles.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 6730a85547..22f9f2d7c0 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -6,7 +6,6 @@ import { bodyFontColor, borderHoverColor, borderWidth, - smallDelay, smallPadding, tooltipCaptionFont } from '@ni/nimble-components/dist/esm/theme-provider/design-tokens'; From 179c2e2c7b2b36e86a48b96b944da711ea83f926 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:36:36 +0000 Subject: [PATCH 14/26] Show slider range labels while active Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/styles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 22f9f2d7c0..8322ced2b7 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -134,7 +134,8 @@ export const styles = css` inset-inline-end: 0; } - :host(:hover) .range-label { + :host(:hover) .range-label, + :host(:active) .range-label { display: block; } From 3e5bf1fca67e9ea3fb4ba58057e46a27cbefd972 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:48:00 +0000 Subject: [PATCH 15/26] Add slider coverage and CSS convention fixes Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- .../ok-components/src/th/slider/styles.ts | 20 +++- .../src/th/slider/tests/slider.spec.ts | 105 +++++++++++++++--- 2 files changed, 107 insertions(+), 18 deletions(-) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 8322ced2b7..4e141a7b76 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -134,7 +134,10 @@ export const styles = css` inset-inline-end: 0; } - :host(:hover) .range-label, + :host(:hover) .range-label { + display: block; + } + :host(:active) .range-label { display: block; } @@ -183,10 +186,15 @@ export const styles = css` } @layer hover { - :host(:not([disabled]):not([readonly]):hover) .thumb-container { + :host(:hover) .thumb-container { border-color: ${borderHoverColor}; box-shadow: inset 0 0 0 ${borderWidth} ${borderHoverColor}; } + + :host([readonly]:hover) .thumb-container { + border-color: var(--ni-private-slider-thumb-border-selected-color); + box-shadow: none; + } } @layer focusVisible { @@ -204,13 +212,16 @@ export const styles = css` } @layer active { - :host(:not([disabled]):not([readonly]):active) - .thumb-container::after { + :host(:active) .thumb-container::after { background-color: var( --ni-private-slider-thumb-background-active-color ); opacity: 1; } + + :host([readonly]:active) .thumb-container::after { + opacity: 0; + } } @layer disabled { @@ -235,6 +246,7 @@ export const styles = css` border: ${borderWidth} solid var(--ni-private-slider-thumb-border-disabled-color); background-color: ${applicationBackgroundColor}; + box-shadow: none; } :host([disabled]) .thumb-container::after { diff --git a/packages/ok-components/src/th/slider/tests/slider.spec.ts b/packages/ok-components/src/th/slider/tests/slider.spec.ts index 1f192821c4..3f9f397c3d 100644 --- a/packages/ok-components/src/th/slider/tests/slider.spec.ts +++ b/packages/ok-components/src/th/slider/tests/slider.spec.ts @@ -1,31 +1,94 @@ import { html } from '@ni/fast-element'; +import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers'; import { Slider, sliderTag } from '..'; -import { fixture } from '../../../utilities/tests/fixture'; +import { fixture, type Fixture } from '../../../utilities/tests/fixture'; + +async function setup( + markup = html`<${sliderTag}></${sliderTag}>` +): Promise<Fixture<Slider>> { + return await fixture<Slider>(markup); +} describe('Slider', () => { + let element: Slider; + let connect: () => Promise<void>; + let disconnect: (() => Promise<void>) | undefined; + + afterEach(async () => { + await disconnect?.(); + disconnect = undefined; + }); + it('can construct an element instance', () => { expect(document.createElement(sliderTag)).toBeInstanceOf(Slider); }); it('provides the FAST Foundation slider template', async () => { - const { element, connect, disconnect } = await fixture<Slider>( - html`<${sliderTag}></${sliderTag}>` - ); + ({ element, connect, disconnect } = await setup()); + await connect(); + + expect( + element.shadowRoot?.querySelector('[part="track-container"]') + ).not.toBeNull(); + expect( + element.shadowRoot?.querySelector('[part="track-start"]') + ).not.toBeNull(); + expect( + element.shadowRoot?.querySelector('[part="thumb-container"]') + ).not.toBeNull(); + expect( + element.shadowRoot?.querySelector('.minimum-label')?.textContent?.trim() + ).toBe('0'); + expect( + element.shadowRoot?.querySelector('.maximum-label')?.textContent?.trim() + ).toBe('10'); + }); + + it('sets slider accessibility attributes', async () => { + ({ element, connect, disconnect } = await setup( + html`<${sliderTag} + min="2" + max="12" + value="6" + orientation="vertical" + ></${sliderTag}>` + )); + element.valueTextFormatter = value => `${value} units`; await connect(); + await waitForUpdatesAsync(); - expect(element.shadowRoot?.querySelector('[part="track-container"]')).not.toBeNull(); - expect(element.shadowRoot?.querySelector('[part="track-start"]')).not.toBeNull(); - expect(element.shadowRoot?.querySelector('[part="thumb-container"]')).not.toBeNull(); - expect(element.shadowRoot?.querySelector('.minimum-label')?.textContent?.trim()).toBe('0'); - expect(element.shadowRoot?.querySelector('.maximum-label')?.textContent?.trim()).toBe('10'); + expect(element.getAttribute('role')).toBe('slider'); + expect(element.getAttribute('aria-valuemin')).toBe('2'); + expect(element.getAttribute('aria-valuemax')).toBe('12'); + expect(element.getAttribute('aria-valuenow')).toBe('6'); + expect(element.getAttribute('aria-valuetext')).toBe('6 units'); + expect(element.getAttribute('aria-orientation')).toBe('vertical'); + }); + + it('sets disabled accessibility attributes and removes tabindex', async () => { + ({ element, connect, disconnect } = await setup( + html`<${sliderTag} disabled></${sliderTag}>` + )); + await connect(); + + expect(element.getAttribute('aria-disabled')).toBe('true'); + expect(element.hasAttribute('tabindex')).toBeFalse(); + }); + + it('sets readonly accessibility attributes', async () => { + ({ element, connect, disconnect } = await setup( + html`<${sliderTag} readonly></${sliderTag}>` + )); + await connect(); - await disconnect(); + expect(element.getAttribute('aria-readonly')).toBe('true'); + expect(element.getAttribute('tabindex')).toBe('0'); }); it('can display the current value next to the thumb', async () => { - const { element, connect, disconnect } = await fixture<Slider>( + ({ element, connect, disconnect } = await setup( html`<${sliderTag} value="4" value-visible></${sliderTag}>` - ); + )); await connect(); expect(element.valueVisible).toBeTrue(); @@ -34,11 +97,25 @@ describe('Slider', () => { ).toBe('4'); element.value = '7'; - await Promise.resolve(); + await waitForUpdatesAsync(); expect( element.shadowRoot?.querySelector('.value-label')?.textContent?.trim() ).toBe('7'); + }); + + it('updates range labels when min and max change', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); - await disconnect(); + element.min = 3; + element.max = 9; + await waitForUpdatesAsync(); + + expect( + element.shadowRoot?.querySelector('.minimum-label')?.textContent?.trim() + ).toBe('3'); + expect( + element.shadowRoot?.querySelector('.maximum-label')?.textContent?.trim() + ).toBe('9'); }); }); From e628e36ece55740f4f4e82a43e185bcea3e3555b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:50:18 +0000 Subject: [PATCH 16/26] Clarify disabled slider hover styles Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/styles.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 4e141a7b76..1344630fab 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -195,6 +195,11 @@ export const styles = css` border-color: var(--ni-private-slider-thumb-border-selected-color); box-shadow: none; } + + :host([disabled]:hover) .thumb-container { + border-color: var(--ni-private-slider-thumb-border-disabled-color); + box-shadow: none; + } } @layer focusVisible { From b563e06a60545c7b3d7ac98d2f31b74d17cc118b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:52:33 +0000 Subject: [PATCH 17/26] Address slider review clarity Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/ok-components/src/th/slider/styles.ts | 4 ++++ packages/ok-components/src/th/slider/tests/slider.spec.ts | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 1344630fab..083c79b9b0 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -227,6 +227,10 @@ export const styles = css` :host([readonly]:active) .thumb-container::after { opacity: 0; } + + :host([disabled]:active) .thumb-container::after { + opacity: 0; + } } @layer disabled { diff --git a/packages/ok-components/src/th/slider/tests/slider.spec.ts b/packages/ok-components/src/th/slider/tests/slider.spec.ts index 3f9f397c3d..4ade39de71 100644 --- a/packages/ok-components/src/th/slider/tests/slider.spec.ts +++ b/packages/ok-components/src/th/slider/tests/slider.spec.ts @@ -11,11 +11,12 @@ async function setup( describe('Slider', () => { let element: Slider; - let connect: () => Promise<void>; + let connect: () => Promise<void> = async () => {}; let disconnect: (() => Promise<void>) | undefined; afterEach(async () => { await disconnect?.(); + connect = async () => {}; disconnect = undefined; }); From 80ad2d99d89f9719525308c8602824f59fc6c1c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:54:55 +0000 Subject: [PATCH 18/26] Simplify slider test fixtures Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- .../src/th/slider/tests/slider.spec.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/ok-components/src/th/slider/tests/slider.spec.ts b/packages/ok-components/src/th/slider/tests/slider.spec.ts index 4ade39de71..b140dce256 100644 --- a/packages/ok-components/src/th/slider/tests/slider.spec.ts +++ b/packages/ok-components/src/th/slider/tests/slider.spec.ts @@ -10,13 +10,10 @@ async function setup( } describe('Slider', () => { - let element: Slider; - let connect: () => Promise<void> = async () => {}; let disconnect: (() => Promise<void>) | undefined; afterEach(async () => { await disconnect?.(); - connect = async () => {}; disconnect = undefined; }); @@ -25,7 +22,9 @@ describe('Slider', () => { }); it('provides the FAST Foundation slider template', async () => { - ({ element, connect, disconnect } = await setup()); + const fixtureResult = await setup(); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; await connect(); expect( @@ -46,14 +45,16 @@ describe('Slider', () => { }); it('sets slider accessibility attributes', async () => { - ({ element, connect, disconnect } = await setup( + const fixtureResult = await setup( html`<${sliderTag} min="2" max="12" value="6" orientation="vertical" ></${sliderTag}>` - )); + ); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; element.valueTextFormatter = value => `${value} units`; await connect(); await waitForUpdatesAsync(); @@ -67,9 +68,11 @@ describe('Slider', () => { }); it('sets disabled accessibility attributes and removes tabindex', async () => { - ({ element, connect, disconnect } = await setup( + const fixtureResult = await setup( html`<${sliderTag} disabled></${sliderTag}>` - )); + ); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; await connect(); expect(element.getAttribute('aria-disabled')).toBe('true'); @@ -77,9 +80,11 @@ describe('Slider', () => { }); it('sets readonly accessibility attributes', async () => { - ({ element, connect, disconnect } = await setup( + const fixtureResult = await setup( html`<${sliderTag} readonly></${sliderTag}>` - )); + ); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; await connect(); expect(element.getAttribute('aria-readonly')).toBe('true'); @@ -87,9 +92,11 @@ describe('Slider', () => { }); it('can display the current value next to the thumb', async () => { - ({ element, connect, disconnect } = await setup( + const fixtureResult = await setup( html`<${sliderTag} value="4" value-visible></${sliderTag}>` - )); + ); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; await connect(); expect(element.valueVisible).toBeTrue(); @@ -105,7 +112,9 @@ describe('Slider', () => { }); it('updates range labels when min and max change', async () => { - ({ element, connect, disconnect } = await setup()); + const fixtureResult = await setup(); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; await connect(); element.min = 3; From e370db16619c86bf8c4c00a4f03f53f8f307072e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:05:20 +0000 Subject: [PATCH 19/26] Combine slider orientation matrix states Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- .../src/ok/th/slider/slider-matrix.stories.ts | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts index 2b60a0bb7a..5d2b02db51 100644 --- a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts @@ -19,6 +19,7 @@ const metadata: Meta = { export default metadata; type SliderState = readonly [string, boolean, boolean]; +type OrientationState = readonly [string, 'horizontal' | 'vertical']; const states: SliderState[] = [ ['Enabled', false, false], @@ -26,14 +27,27 @@ const states: SliderState[] = [ ['Disabled', true, false] ]; -const interactionStates = cartesianProduct([states] as const); +const orientationStates: OrientationState[] = [ + ['Horizontal', 'horizontal'], + ['Vertical', 'vertical'] +]; + +const interactionStates = cartesianProduct([orientationStates, states] as const); + +const getSliderStyle = (orientation: OrientationState[1]): string => ( + orientation === 'horizontal' ? 'width: 200px;' : 'height: 200px;' +); -const component = ([name, disabled, readOnly]: SliderState): ViewTemplate => html` +const component = ( + [orientationName, orientation]: OrientationState, + [stateName, disabled, readOnly]: SliderState +): ViewTemplate => html` <div style="display: inline-flex; flex-direction: column; gap: 8px; margin: 8px;"> - <span>${() => name}</span> + <span>${() => `${orientationName} ${stateName}`}</span> <${sliderTag} - style="width: 200px;" + style="${() => getSliderStyle(orientation)}" value="40" + orientation="${() => orientation}" ?disabled="${() => disabled}" ?readonly="${() => readOnly}" value-visible @@ -41,17 +55,10 @@ const component = ([name, disabled, readOnly]: SliderState): ViewTemplate => htm </div> `; -export const statesThemeMatrix: StoryFn = createMatrixThemeStory( - createMatrix(component, [states]) +export const themeMatrix: StoryFn = createMatrixThemeStory( + createMatrix(component, [orientationStates, states]) ); -export const orientationsThemeMatrix: StoryFn = createMatrixThemeStory(html` - <div style="display: flex; gap: 32px; align-items: flex-start; padding: 16px;"> - <${sliderTag} value="40" value-visible></${sliderTag}> - <${sliderTag} value="40" orientation="vertical" value-visible></${sliderTag}> - </div> -`); - export const interactionsThemeMatrix: StoryFn = createMatrixThemeStory( createMatrixInteractionsFromStates(component, { hover: interactionStates, From e3860f33a34f9ebd5bc599452ba163b3f819e611 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:06:23 +0000 Subject: [PATCH 20/26] Name slider matrix size Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/storybook/src/ok/th/slider/slider-matrix.stories.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts index 5d2b02db51..2e5eccff9c 100644 --- a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts @@ -32,10 +32,12 @@ const orientationStates: OrientationState[] = [ ['Vertical', 'vertical'] ]; +const sliderSize = '200px'; + const interactionStates = cartesianProduct([orientationStates, states] as const); const getSliderStyle = (orientation: OrientationState[1]): string => ( - orientation === 'horizontal' ? 'width: 200px;' : 'height: 200px;' + orientation === 'horizontal' ? `width: ${sliderSize};` : `height: ${sliderSize};` ); const component = ( From 00ca867d9f45d2c8f9ebfb8dd29eded4a6d106de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:43:17 +0000 Subject: [PATCH 21/26] Disable Vite HMR in karma config to fix flaky test disconnects Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/nimble-components/karma.conf.cjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/nimble-components/karma.conf.cjs b/packages/nimble-components/karma.conf.cjs index 55d07564a0..5b90f17f9f 100644 --- a/packages/nimble-components/karma.conf.cjs +++ b/packages/nimble-components/karma.conf.cjs @@ -126,6 +126,13 @@ module.exports = config => { alias: { '/base': '', } + }, + server: { + // Disable HMR's WebSocket server. It is unused during single-run test + // execution and otherwise defaults to a fixed port (24678) that can + // collide when multiple karma-vite instances run concurrently in CI, + // causing browsers to disconnect. + hmr: false } } } From dfea43a6fe749f2b50002a109a958e6fc88c4be8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:46:05 +0000 Subject: [PATCH 22/26] Revert nimble-components test workaround Co-authored-by: atmgrifter00 <26874831+atmgrifter00@users.noreply.github.com> --- packages/nimble-components/karma.conf.cjs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/nimble-components/karma.conf.cjs b/packages/nimble-components/karma.conf.cjs index 5b90f17f9f..55d07564a0 100644 --- a/packages/nimble-components/karma.conf.cjs +++ b/packages/nimble-components/karma.conf.cjs @@ -126,13 +126,6 @@ module.exports = config => { alias: { '/base': '', } - }, - server: { - // Disable HMR's WebSocket server. It is unused during single-run test - // execution and otherwise defaults to a fixed port (24678) that can - // collide when multiple karma-vite instances run concurrently in CI, - // causing browsers to disconnect. - hmr: false } } } From e93ac8689e245792e9bd1403a94ca9a6410e5de4 Mon Sep 17 00:00:00 2001 From: Jonathan Meyer <jonathan.meyer@emerson.com> Date: Sun, 30 Aug 2026 09:01:14 -0500 Subject: [PATCH 23/26] Matrix test label style fix --- packages/storybook/src/ok/th/slider/slider-matrix.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts index 2e5eccff9c..22ff588284 100644 --- a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts @@ -45,7 +45,7 @@ const component = ( [stateName, disabled, readOnly]: SliderState ): ViewTemplate => html` <div style="display: inline-flex; flex-direction: column; gap: 8px; margin: 8px;"> - <span>${() => `${orientationName} ${stateName}`}</span> + <span style="font-size: 12px; color: #0076d6; text-align: center; min-height: 16px;">${() => `${orientationName} ${stateName}`}</span> <${sliderTag} style="${() => getSliderStyle(orientation)}" value="40" From 03fdb3ff48f34574099f4828de081202cad1a94e Mon Sep 17 00:00:00 2001 From: Jonathan Meyer <jonathan.meyer@emerson.com> Date: Sun, 30 Aug 2026 12:07:49 -0500 Subject: [PATCH 24/26] Fix vertical slider. --- packages/ok-components/src/th/slider/index.ts | 44 ++++++++++++ .../ok-components/src/th/slider/styles.ts | 8 ++- .../ok-components/src/th/slider/template.ts | 11 ++- .../src/th/slider/tests/slider.spec.ts | 72 +++++++++++++++++++ .../src/ok/th/slider/slider-matrix.stories.ts | 2 +- .../src/ok/th/slider/slider.stories.ts | 3 +- 6 files changed, 133 insertions(+), 7 deletions(-) diff --git a/packages/ok-components/src/th/slider/index.ts b/packages/ok-components/src/th/slider/index.ts index 72d4098ac8..fef09f39ff 100644 --- a/packages/ok-components/src/th/slider/index.ts +++ b/packages/ok-components/src/th/slider/index.ts @@ -4,9 +4,15 @@ import { Slider as FoundationSlider, type SliderOptions } from '@ni/fast-foundation'; +import { Orientation } from '@ni/fast-web-utilities'; import { styles } from './styles'; import { template } from './template'; +interface FoundationSliderInternals { + calculateNewValue: (rawValue: number) => number; + convertToConstrainedValue: (value: number) => number; +} + declare global { interface HTMLElementTagNameMap { 'ok-th-slider': Slider; @@ -26,6 +32,18 @@ export class Slider extends FoundationSlider { /** @internal */ public valueLabel?: HTMLSpanElement; + public constructor() { + super(); + + const internals = this as unknown as FoundationSliderInternals; + const calculateFoundationValue = internals.calculateNewValue; + internals.calculateNewValue = rawValue => calculateFoundationValue( + this.orientation === Orientation.vertical + ? this.trackMinHeight + this.trackHeight - rawValue + : rawValue + ); + } + /** @internal */ public override connectedCallback(): void { super.connectedCallback(); @@ -38,6 +56,32 @@ export class Slider extends FoundationSlider { this.updateValueLabel(); } + /** @internal */ + public override increment(): void { + if (this.orientation !== Orientation.vertical) { + super.increment(); + return; + } + + this.setVerticalValue(Number(this.value) + Number(this.step)); + } + + /** @internal */ + public override decrement(): void { + if (this.orientation !== Orientation.vertical) { + super.decrement(); + return; + } + + this.setVerticalValue(Number(this.value) - Number(this.step)); + } + + private setVerticalValue(value: number): void { + const { convertToConstrainedValue } = this as unknown as FoundationSliderInternals; + const constrainedValue = convertToConstrainedValue(value); + this.value = `${Math.min(this.max, Math.max(this.min, constrainedValue))}`; + } + private updateValueLabel(): void { if (this.valueLabel) { this.valueLabel.textContent = this.value; diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 083c79b9b0..16b36ee164 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -152,14 +152,15 @@ export const styles = css` } :host([orientation='vertical']) .track-start { - inset-block-start: 0; + inset-block-start: auto; + inset-block-end: 0; inset-inline: 0; } :host([orientation='vertical']) .thumb-container { top: auto; left: 50%; - transform: translate(-50%, 50%); + transform: translate(-50%, -50%); } :host([orientation='vertical']) .value-label { @@ -168,7 +169,8 @@ export const styles = css` } :host([orientation='vertical']) .range-label { - inset-inline-start: calc(50% + 8px); + inset-inline-start: auto; + inset-inline-end: calc(50% + 8px); } :host([orientation='vertical']) .minimum-label { diff --git a/packages/ok-components/src/th/slider/template.ts b/packages/ok-components/src/th/slider/template.ts index f6d2ee3073..089efd9227 100644 --- a/packages/ok-components/src/th/slider/template.ts +++ b/packages/ok-components/src/th/slider/template.ts @@ -1,6 +1,13 @@ import { html, ref } from '@ni/fast-element'; +import { Orientation } from '@ni/fast-web-utilities'; import type { Slider } from '.'; +const getPosition = (slider: Slider): string => ( + slider.orientation === Orientation.vertical + ? (slider.position?.replace('bottom:', 'top:') ?? '') + : slider.position +); + export const template = html<Slider>` <template role="slider" @@ -17,7 +24,7 @@ export const template = html<Slider>` <div part="positioning-region" class="positioning-region"> <div ${ref('track')} part="track-container" class="track"> <slot name="track"></slot> - <div part="track-start" class="track-start" style="${x => x.position}"> + <div part="track-start" class="track-start" style="${getPosition}"> <slot name="track-start"></slot> </div> </div> @@ -28,7 +35,7 @@ export const template = html<Slider>` ${ref('thumb')} part="thumb-container" class="thumb-container" - style="${x => x.position}" + style="${getPosition}" > <slot name="thumb"></slot> <span ${ref('valueLabel')} class="value-label" aria-hidden="true"></span> diff --git a/packages/ok-components/src/th/slider/tests/slider.spec.ts b/packages/ok-components/src/th/slider/tests/slider.spec.ts index b140dce256..c790983ec8 100644 --- a/packages/ok-components/src/th/slider/tests/slider.spec.ts +++ b/packages/ok-components/src/th/slider/tests/slider.spec.ts @@ -67,6 +67,78 @@ describe('Slider', () => { expect(element.getAttribute('aria-orientation')).toBe('vertical'); }); + it('positions a vertical slider from minimum at the bottom to maximum at the top', async () => { + const fixtureResult = await setup( + html`<${sliderTag} + min="2" + max="12" + value="2" + orientation="vertical" + ></${sliderTag}>` + ); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; + await connect(); + await waitForUpdatesAsync(); + + const trackStart = element.shadowRoot?.querySelector<HTMLElement>( + '[part="track-start"]' + ); + const thumb = element.shadowRoot?.querySelector<HTMLElement>( + '[part="thumb-container"]' + ); + expect(trackStart?.style.top).toBe('100%'); + expect(thumb?.style.top).toBe('100%'); + + element.value = '12'; + await waitForUpdatesAsync(); + + expect(trackStart?.style.top).toBe('0%'); + expect(thumb?.style.top).toBe('0%'); + }); + + it('increases a vertical slider value when dragged upward', async () => { + const fixtureResult = await setup( + html`<${sliderTag} orientation="vertical"></${sliderTag}>` + ); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; + await connect(); + + spyOn(element.track, 'getBoundingClientRect').and.returnValue( + new DOMRect(0, 100, 4, 200) + ); + element.dispatchEvent( + new MouseEvent('mousedown', { bubbles: true, clientY: 300 }) + ); + window.dispatchEvent(new MouseEvent('mousemove', { clientY: 100 })); + await waitForUpdatesAsync(); + + expect(element.value).toBe('10'); + }); + + it('uses up and down arrow keys to increase and decrease a vertical slider', async () => { + const fixtureResult = await setup( + html`<${sliderTag} + value="5" + orientation="vertical" + ></${sliderTag}>` + ); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; + await connect(); + + element.dispatchEvent( + new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }) + ); + expect(element.value).toBe('6'); + + element.dispatchEvent( + new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }) + ); + expect(element.value).toBe('5'); + }); + it('sets disabled accessibility attributes and removes tabindex', async () => { const fixtureResult = await setup( html`<${sliderTag} disabled></${sliderTag}>` diff --git a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts index 22ff588284..9716a20622 100644 --- a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts @@ -48,7 +48,7 @@ const component = ( <span style="font-size: 12px; color: #0076d6; text-align: center; min-height: 16px;">${() => `${orientationName} ${stateName}`}</span> <${sliderTag} style="${() => getSliderStyle(orientation)}" - value="40" + value="4" orientation="${() => orientation}" ?disabled="${() => disabled}" ?readonly="${() => readOnly}" diff --git a/packages/storybook/src/ok/th/slider/slider.stories.ts b/packages/storybook/src/ok/th/slider/slider.stories.ts index 629a0e611d..2e748bd7b0 100644 --- a/packages/storybook/src/ok/th/slider/slider.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider.stories.ts @@ -35,6 +35,7 @@ const metadata: Meta<SliderArgs> = { statusLink: './?path=/docs/component-status--docs#ok-components' })} <${sliderTag} + style="${x => x.orientation === 'horizontal' ? `width: 200px;` : `height: 200px;`}" value="${x => x.value}" min="${x => x.min}" max="${x => x.max}" @@ -86,7 +87,7 @@ const metadata: Meta<SliderArgs> = { } }, args: { - value: 50, + value: 40, min: 0, max: 100, step: 1, From 4223a279a4157c6d06c1956a4b6e6543c68d0645 Mon Sep 17 00:00:00 2001 From: Jonathan Meyer <jonathan.meyer@emerson.com> Date: Sun, 30 Aug 2026 15:36:47 -0500 Subject: [PATCH 25/26] Add `showMinMax` and fix some styling issues. --- packages/ok-components/src/th/slider/index.ts | 9 ++++ .../ok-components/src/th/slider/styles.ts | 50 +++++++++++-------- .../ok-components/src/th/slider/template.ts | 7 ++- .../src/th/slider/tests/slider.spec.ts | 40 +++++++++++++++ packages/ok-components/src/th/slider/types.ts | 7 +++ .../src/ok/th/slider/slider.stories.ts | 24 +++++++-- 6 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 packages/ok-components/src/th/slider/types.ts diff --git a/packages/ok-components/src/th/slider/index.ts b/packages/ok-components/src/th/slider/index.ts index fef09f39ff..d3959ab5a7 100644 --- a/packages/ok-components/src/th/slider/index.ts +++ b/packages/ok-components/src/th/slider/index.ts @@ -7,6 +7,9 @@ import { import { Orientation } from '@ni/fast-web-utilities'; import { styles } from './styles'; import { template } from './template'; +import { SliderShowMinMax } from './types'; + +export { SliderShowMinMax }; interface FoundationSliderInternals { calculateNewValue: (rawValue: number) => number; @@ -29,6 +32,12 @@ export class Slider extends FoundationSlider { @attr({ attribute: 'value-visible', mode: 'boolean' }) public valueVisible = false; + /** + * Controls when the minimum and maximum value labels are displayed. + */ + @attr({ attribute: 'show-min-max' }) + public showMinMax: SliderShowMinMax = SliderShowMinMax.hover; + /** @internal */ public valueLabel?: HTMLSpanElement; diff --git a/packages/ok-components/src/th/slider/styles.ts b/packages/ok-components/src/th/slider/styles.ts index 16b36ee164..6d36b0291a 100644 --- a/packages/ok-components/src/th/slider/styles.ts +++ b/packages/ok-components/src/th/slider/styles.ts @@ -6,6 +6,7 @@ import { bodyFontColor, borderHoverColor, borderWidth, + fillSelectedColor, smallPadding, tooltipCaptionFont } from '@ni/nimble-components/dist/esm/theme-provider/design-tokens'; @@ -104,12 +105,13 @@ export const styles = css` .value-label { position: absolute; - inset-block-end: calc(100% + ${smallPadding}); + inset-block-end: calc(50% + 7px + ${smallPadding}); display: none; font: ${tooltipCaptionFont}; line-height: 1; white-space: nowrap; pointer-events: none; + transform: translateX(50%); } :host([value-visible]) .value-label { @@ -118,7 +120,7 @@ export const styles = css` .range-label { position: absolute; - inset-block-start: calc(50% + 8px); + inset-block-start: calc(50% + 7px + ${smallPadding}); display: none; font: ${tooltipCaptionFont}; line-height: 1; @@ -128,17 +130,22 @@ export const styles = css` .minimum-label { inset-inline-start: 0; + transform: translateX(-50%); } .maximum-label { inset-inline-end: 0; + transform: translateX(50%); } - :host(:hover) .range-label { + :host([show-min-max='always']) .range-label { display: block; } - :host(:active) .range-label { + :host(:not([show-min-max]):hover) .range-label, + :host(:not([show-min-max]):active) .range-label, + :host([show-min-max='hover']:hover) .range-label, + :host([show-min-max='hover']:active) .range-label { display: block; } @@ -165,20 +172,23 @@ export const styles = css` :host([orientation='vertical']) .value-label { inset-block-end: auto; - inset-inline-start: calc(100% + ${smallPadding}); + inset-inline-start: calc(50% + 7px + ${smallPadding}); + transform: translateY(-50%); } :host([orientation='vertical']) .range-label { inset-inline-start: auto; - inset-inline-end: calc(50% + 8px); + inset-inline-end: calc(50% + 7px + ${smallPadding}); } :host([orientation='vertical']) .minimum-label { inset-block: auto 0; + transform: translateY(50%); } :host([orientation='vertical']) .maximum-label { inset-block: 0 auto; + transform: translateY(-50%); } ::slotted(*) { @@ -189,36 +199,36 @@ export const styles = css` @layer hover { :host(:hover) .thumb-container { - border-color: ${borderHoverColor}; - box-shadow: inset 0 0 0 ${borderWidth} ${borderHoverColor}; + border: calc(${borderWidth} * 2) solid ${borderHoverColor}; } :host([readonly]:hover) .thumb-container { - border-color: var(--ni-private-slider-thumb-border-selected-color); - box-shadow: none; + border: ${borderWidth} solid + var(--ni-private-slider-thumb-border-selected-color); } :host([disabled]:hover) .thumb-container { - border-color: var(--ni-private-slider-thumb-border-disabled-color); - box-shadow: none; + border: ${borderWidth} solid + var(--ni-private-slider-thumb-border-disabled-color); } } @layer focusVisible { :host(${focusVisible}) .thumb-container { - border-color: ${borderHoverColor}; - box-shadow: inset 0 0 0 ${borderWidth} ${borderHoverColor}; + border: calc(${borderWidth} * 2) solid ${borderHoverColor}; } :host(${focusVisible}) .thumb-container::after { - border: ${borderWidth} solid - var(--ni-private-slider-thumb-border-selected-color); - background-color: ${applicationBackgroundColor}; + background-color: ${fillSelectedColor}; opacity: 1; } } @layer active { + :host(:active) .thumb-container { + border-width: calc(${borderWidth} * 2); + } + :host(:active) .thumb-container::after { background-color: var( --ni-private-slider-thumb-background-active-color @@ -226,10 +236,11 @@ export const styles = css` opacity: 1; } - :host([readonly]:active) .thumb-container::after { - opacity: 0; + :host([readonly]:active) .thumb-container { + border-width: ${borderWidth}; } + :host([readonly]:active) .thumb-container::after, :host([disabled]:active) .thumb-container::after { opacity: 0; } @@ -257,7 +268,6 @@ export const styles = css` border: ${borderWidth} solid var(--ni-private-slider-thumb-border-disabled-color); background-color: ${applicationBackgroundColor}; - box-shadow: none; } :host([disabled]) .thumb-container::after { diff --git a/packages/ok-components/src/th/slider/template.ts b/packages/ok-components/src/th/slider/template.ts index 089efd9227..90c4b57d97 100644 --- a/packages/ok-components/src/th/slider/template.ts +++ b/packages/ok-components/src/th/slider/template.ts @@ -38,8 +38,13 @@ export const template = html<Slider>` style="${getPosition}" > <slot name="thumb"></slot> - <span ${ref('valueLabel')} class="value-label" aria-hidden="true"></span> </div> + <span + ${ref('valueLabel')} + class="value-label" + style="${getPosition}" + aria-hidden="true" + ></span> </div> </template> `; diff --git a/packages/ok-components/src/th/slider/tests/slider.spec.ts b/packages/ok-components/src/th/slider/tests/slider.spec.ts index c790983ec8..de56c83e9e 100644 --- a/packages/ok-components/src/th/slider/tests/slider.spec.ts +++ b/packages/ok-components/src/th/slider/tests/slider.spec.ts @@ -1,6 +1,7 @@ import { html } from '@ni/fast-element'; import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers'; import { Slider, sliderTag } from '..'; +import { SliderShowMinMax } from '../types'; import { fixture, type Fixture } from '../../../utilities/tests/fixture'; async function setup( @@ -200,4 +201,43 @@ describe('Slider', () => { element.shadowRoot?.querySelector('.maximum-label')?.textContent?.trim() ).toBe('9'); }); + + it('defaults to showing min and max labels on hover', async () => { + const fixtureResult = await setup(); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; + await connect(); + + expect(element.showMinMax).toBe(SliderShowMinMax.hover); + }); + + it('can always show min and max labels', async () => { + const fixtureResult = await setup( + html`<${sliderTag} show-min-max="always"></${sliderTag}>` + ); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; + await connect(); + + const minimumLabel = element.shadowRoot?.querySelector('.minimum-label'); + const maximumLabel = element.shadowRoot?.querySelector('.maximum-label'); + expect(element.showMinMax).toBe(SliderShowMinMax.always); + expect(getComputedStyle(minimumLabel!).display).toBe('block'); + expect(getComputedStyle(maximumLabel!).display).toBe('block'); + }); + + it('can never show min and max labels', async () => { + const fixtureResult = await setup( + html`<${sliderTag} show-min-max="never"></${sliderTag}>` + ); + const { element, connect } = fixtureResult; + disconnect = fixtureResult.disconnect; + await connect(); + + const minimumLabel = element.shadowRoot?.querySelector('.minimum-label'); + const maximumLabel = element.shadowRoot?.querySelector('.maximum-label'); + expect(element.showMinMax).toBe(SliderShowMinMax.never); + expect(getComputedStyle(minimumLabel!).display).toBe('none'); + expect(getComputedStyle(maximumLabel!).display).toBe('none'); + }); }); diff --git a/packages/ok-components/src/th/slider/types.ts b/packages/ok-components/src/th/slider/types.ts new file mode 100644 index 0000000000..03c93914fd --- /dev/null +++ b/packages/ok-components/src/th/slider/types.ts @@ -0,0 +1,7 @@ +export const SliderShowMinMax = { + always: 'always', + hover: 'hover', + never: 'never' +} as const; + +export type SliderShowMinMax = (typeof SliderShowMinMax)[keyof typeof SliderShowMinMax]; \ No newline at end of file diff --git a/packages/storybook/src/ok/th/slider/slider.stories.ts b/packages/storybook/src/ok/th/slider/slider.stories.ts index 2e748bd7b0..d01f3bc5e6 100644 --- a/packages/storybook/src/ok/th/slider/slider.stories.ts +++ b/packages/storybook/src/ok/th/slider/slider.stories.ts @@ -1,6 +1,10 @@ import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html-vite'; import { html } from '@ni/fast-element'; -import { sliderTag } from '@ni/ok-components/dist/esm/th/slider'; +import { + sliderTag, + SliderShowMinMax, + type SliderShowMinMax as SliderShowMinMaxType +} from '@ni/ok-components/dist/esm/th/slider'; import { withActions } from 'storybook/actions/decorator'; import { apiCategory, @@ -18,9 +22,14 @@ interface SliderArgs { disabled: boolean; readOnly: boolean; valueVisible: boolean; + showMinMax: SliderShowMinMaxType; change?: (event: Event) => void; } +const getSliderStyle = (orientation: SliderArgs['orientation']): string => ( + orientation === 'horizontal' ? 'width: 200px;' : 'height: 200px;' +); + const metadata: Meta<SliderArgs> = { title: 'Ok/Th Slider', decorators: [withActions<HtmlRenderer>], @@ -35,7 +44,7 @@ const metadata: Meta<SliderArgs> = { statusLink: './?path=/docs/component-status--docs#ok-components' })} <${sliderTag} - style="${x => x.orientation === 'horizontal' ? `width: 200px;` : `height: 200px;`}" + style="${x => getSliderStyle(x.orientation)}" value="${x => x.value}" min="${x => x.min}" max="${x => x.max}" @@ -44,6 +53,7 @@ const metadata: Meta<SliderArgs> = { ?disabled="${x => x.disabled}" ?readonly="${x => x.readOnly}" ?value-visible="${x => x.valueVisible}" + show-min-max="${x => x.showMinMax}" ></${sliderTag}> `), argTypes: { @@ -81,6 +91,13 @@ const metadata: Meta<SliderArgs> = { description: 'Displays the current value next to the slider thumb.', table: { category: apiCategory.attributes } }, + showMinMax: { + name: 'show-min-max', + description: 'Controls when the minimum and maximum value labels are displayed.', + options: Object.values(SliderShowMinMax), + control: { type: 'radio' }, + table: { category: apiCategory.attributes } + }, change: { table: { category: apiCategory.events }, control: false @@ -94,7 +111,8 @@ const metadata: Meta<SliderArgs> = { orientation: 'horizontal', disabled: false, readOnly: false, - valueVisible: true + valueVisible: true, + showMinMax: SliderShowMinMax.hover } }; From bac0c0179d4ee41ca9467ee4ea68eeea6dd4f1b8 Mon Sep 17 00:00:00 2001 From: Jonathan Meyer <jonathan.meyer@emerson.com> Date: Sun, 30 Aug 2026 15:53:27 -0500 Subject: [PATCH 26/26] Handling PR feedback. --- packages/ok-components/src/th/slider/index.ts | 42 +++--- .../ok-components/src/th/slider/template.ts | 6 +- .../th/slider/testing/th-slider.pageobject.ts | 64 +++++++++ .../{slider.spec.ts => th-slider.spec.ts} | 132 +++++++----------- packages/ok-components/src/th/slider/types.ts | 4 +- .../ok-react/src/th/slider/index.ts | 16 +-- ...stories.ts => th-slider-matrix.stories.ts} | 6 +- .../th/slider/{slider.mdx => th-slider.mdx} | 2 +- ...slider.stories.ts => th-slider.stories.ts} | 16 +-- 9 files changed, 165 insertions(+), 123 deletions(-) create mode 100644 packages/ok-components/src/th/slider/testing/th-slider.pageobject.ts rename packages/ok-components/src/th/slider/tests/{slider.spec.ts => th-slider.spec.ts} (58%) rename packages/storybook/src/ok/th/slider/{slider-matrix.stories.ts => th-slider-matrix.stories.ts} (94%) rename packages/storybook/src/ok/th/slider/{slider.mdx => th-slider.mdx} (89%) rename packages/storybook/src/ok/th/slider/{slider.stories.ts => th-slider.stories.ts} (92%) diff --git a/packages/ok-components/src/th/slider/index.ts b/packages/ok-components/src/th/slider/index.ts index d3959ab5a7..4ee03de8b6 100644 --- a/packages/ok-components/src/th/slider/index.ts +++ b/packages/ok-components/src/th/slider/index.ts @@ -7,9 +7,9 @@ import { import { Orientation } from '@ni/fast-web-utilities'; import { styles } from './styles'; import { template } from './template'; -import { SliderShowMinMax } from './types'; +import { ThSliderShowMinMax } from './types'; -export { SliderShowMinMax }; +export { ThSliderShowMinMax }; interface FoundationSliderInternals { calculateNewValue: (rawValue: number) => number; @@ -18,14 +18,14 @@ interface FoundationSliderInternals { declare global { interface HTMLElementTagNameMap { - 'ok-th-slider': Slider; + 'ok-th-slider': ThSlider; } } /** * A Nimble-styled slider control. */ -export class Slider extends FoundationSlider { +export class ThSlider extends FoundationSlider { /** * Whether the current value is displayed next to the thumb. */ @@ -36,26 +36,28 @@ export class Slider extends FoundationSlider { * Controls when the minimum and maximum value labels are displayed. */ @attr({ attribute: 'show-min-max' }) - public showMinMax: SliderShowMinMax = SliderShowMinMax.hover; + public showMinMax: ThSliderShowMinMax = ThSliderShowMinMax.hover; /** @internal */ public valueLabel?: HTMLSpanElement; - public constructor() { - super(); - - const internals = this as unknown as FoundationSliderInternals; - const calculateFoundationValue = internals.calculateNewValue; - internals.calculateNewValue = rawValue => calculateFoundationValue( - this.orientation === Orientation.vertical - ? this.trackMinHeight + this.trackHeight - rawValue - : rawValue - ); - } + private verticalMappingInitialized = false; /** @internal */ public override connectedCallback(): void { super.connectedCallback(); + + if (!this.verticalMappingInitialized) { + const internals = this as unknown as FoundationSliderInternals; + const calculateFoundationValue = internals.calculateNewValue; + internals.calculateNewValue = rawValue => calculateFoundationValue( + this.orientation === Orientation.vertical + ? this.trackMinHeight + this.trackHeight - rawValue + : rawValue + ); + this.verticalMappingInitialized = true; + } + this.updateValueLabel(); } @@ -86,8 +88,8 @@ export class Slider extends FoundationSlider { } private setVerticalValue(value: number): void { - const { convertToConstrainedValue } = this as unknown as FoundationSliderInternals; - const constrainedValue = convertToConstrainedValue(value); + const internals = this as unknown as FoundationSliderInternals; + const constrainedValue = internals.convertToConstrainedValue(value); this.value = `${Math.min(this.max, Math.max(this.min, constrainedValue))}`; } @@ -98,7 +100,7 @@ export class Slider extends FoundationSlider { } } -const okThSlider = Slider.compose<SliderOptions>({ +const okThSlider = ThSlider.compose<SliderOptions>({ baseName: 'th-slider', baseClass: FoundationSlider, template, @@ -106,4 +108,4 @@ const okThSlider = Slider.compose<SliderOptions>({ }); DesignSystem.getOrCreate().withPrefix('ok').register(okThSlider()); -export const sliderTag = 'ok-th-slider'; +export const thSliderTag = 'ok-th-slider'; diff --git a/packages/ok-components/src/th/slider/template.ts b/packages/ok-components/src/th/slider/template.ts index 90c4b57d97..830ae8e392 100644 --- a/packages/ok-components/src/th/slider/template.ts +++ b/packages/ok-components/src/th/slider/template.ts @@ -1,14 +1,14 @@ import { html, ref } from '@ni/fast-element'; import { Orientation } from '@ni/fast-web-utilities'; -import type { Slider } from '.'; +import type { ThSlider } from '.'; -const getPosition = (slider: Slider): string => ( +const getPosition = (slider: ThSlider): string => ( slider.orientation === Orientation.vertical ? (slider.position?.replace('bottom:', 'top:') ?? '') : slider.position ); -export const template = html<Slider>` +export const template = html<ThSlider>` <template role="slider" tabindex="${x => (x.disabled ? null : 0)}" diff --git a/packages/ok-components/src/th/slider/testing/th-slider.pageobject.ts b/packages/ok-components/src/th/slider/testing/th-slider.pageobject.ts new file mode 100644 index 0000000000..18c425832a --- /dev/null +++ b/packages/ok-components/src/th/slider/testing/th-slider.pageobject.ts @@ -0,0 +1,64 @@ +import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers'; +import type { ThSlider } from '..'; + +/** + * Page object for ok-th-slider tests. + */ +export class ThSliderPageObject { + public constructor(private readonly sliderElement: ThSlider) {} + + public async pressKey(key: string): Promise<void> { + this.sliderElement.dispatchEvent( + new KeyboardEvent('keydown', { key, bubbles: true }) + ); + await waitForUpdatesAsync(); + } + + public async mouseDown(clientX = 0, clientY = 0): Promise<void> { + this.sliderElement.dispatchEvent( + new MouseEvent('mousedown', { + bubbles: true, + clientX, + clientY + }) + ); + await waitForUpdatesAsync(); + } + + public async mouseMove(clientX = 0, clientY = 0): Promise<void> { + window.dispatchEvent(new MouseEvent('mousemove', { clientX, clientY })); + await waitForUpdatesAsync(); + } + + public getTrackStartPosition(): string { + return this.getElement('[part="track-start"]').style.cssText; + } + + public hasPart(part: string): boolean { + return !!this.sliderElement.shadowRoot?.querySelector(`[part="${part}"]`); + } + + public getThumbPosition(): string { + return this.getElement('[part="thumb-container"]').style.cssText; + } + + public getValueLabelText(): string { + return this.getElement('.value-label').textContent?.trim() ?? ''; + } + + public getRangeLabelText(className: 'minimum-label' | 'maximum-label'): string { + return this.getElement(`.${className}`).textContent?.trim() ?? ''; + } + + public getRangeLabelDisplay(className: 'minimum-label' | 'maximum-label'): string { + return getComputedStyle(this.getElement(`.${className}`)).display; + } + + private getElement<T extends HTMLElement>(selector: string): T { + const element = this.sliderElement.shadowRoot?.querySelector<T>(selector); + if (!element) { + throw new Error(`Expected slider element matching ${selector}`); + } + return element; + } +} diff --git a/packages/ok-components/src/th/slider/tests/slider.spec.ts b/packages/ok-components/src/th/slider/tests/th-slider.spec.ts similarity index 58% rename from packages/ok-components/src/th/slider/tests/slider.spec.ts rename to packages/ok-components/src/th/slider/tests/th-slider.spec.ts index de56c83e9e..58bdb5b8dd 100644 --- a/packages/ok-components/src/th/slider/tests/slider.spec.ts +++ b/packages/ok-components/src/th/slider/tests/th-slider.spec.ts @@ -1,16 +1,18 @@ import { html } from '@ni/fast-element'; import { waitForUpdatesAsync } from '@ni/nimble-components/dist/esm/testing/async-helpers'; -import { Slider, sliderTag } from '..'; -import { SliderShowMinMax } from '../types'; +import { ThSlider, thSliderTag } from '..'; +import { ThSliderShowMinMax } from '../types'; +import { ThSliderPageObject } from '../testing/th-slider.pageobject'; import { fixture, type Fixture } from '../../../utilities/tests/fixture'; async function setup( - markup = html`<${sliderTag}></${sliderTag}>` -): Promise<Fixture<Slider>> { - return await fixture<Slider>(markup); + markup = html`<${thSliderTag}></${thSliderTag}>` +): Promise<Fixture<ThSlider>> { + return await fixture<ThSlider>(markup); } -describe('Slider', () => { +describe('ThSlider', () => { + let pageObject: ThSliderPageObject; let disconnect: (() => Promise<void>) | undefined; afterEach(async () => { @@ -19,7 +21,7 @@ describe('Slider', () => { }); it('can construct an element instance', () => { - expect(document.createElement(sliderTag)).toBeInstanceOf(Slider); + expect(document.createElement(thSliderTag)).toBeInstanceOf(ThSlider); }); it('provides the FAST Foundation slider template', async () => { @@ -27,32 +29,23 @@ describe('Slider', () => { const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; await connect(); + pageObject = new ThSliderPageObject(element); - expect( - element.shadowRoot?.querySelector('[part="track-container"]') - ).not.toBeNull(); - expect( - element.shadowRoot?.querySelector('[part="track-start"]') - ).not.toBeNull(); - expect( - element.shadowRoot?.querySelector('[part="thumb-container"]') - ).not.toBeNull(); - expect( - element.shadowRoot?.querySelector('.minimum-label')?.textContent?.trim() - ).toBe('0'); - expect( - element.shadowRoot?.querySelector('.maximum-label')?.textContent?.trim() - ).toBe('10'); + expect(pageObject.hasPart('track-container')).toBeTrue(); + expect(pageObject.hasPart('track-start')).toBeTrue(); + expect(pageObject.hasPart('thumb-container')).toBeTrue(); + expect(pageObject.getRangeLabelText('minimum-label')).toBe('0'); + expect(pageObject.getRangeLabelText('maximum-label')).toBe('10'); }); it('sets slider accessibility attributes', async () => { const fixtureResult = await setup( - html`<${sliderTag} + html`<${thSliderTag} min="2" max="12" value="6" orientation="vertical" - ></${sliderTag}>` + ></${thSliderTag}>` ); const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; @@ -70,79 +63,69 @@ describe('Slider', () => { it('positions a vertical slider from minimum at the bottom to maximum at the top', async () => { const fixtureResult = await setup( - html`<${sliderTag} + html`<${thSliderTag} min="2" max="12" value="2" orientation="vertical" - ></${sliderTag}>` + ></${thSliderTag}>` ); const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; await connect(); await waitForUpdatesAsync(); - const trackStart = element.shadowRoot?.querySelector<HTMLElement>( - '[part="track-start"]' - ); - const thumb = element.shadowRoot?.querySelector<HTMLElement>( - '[part="thumb-container"]' - ); - expect(trackStart?.style.top).toBe('100%'); - expect(thumb?.style.top).toBe('100%'); + pageObject = new ThSliderPageObject(element); + expect(pageObject.getTrackStartPosition()).toContain('top: 100%'); + expect(pageObject.getThumbPosition()).toContain('top: 100%'); element.value = '12'; await waitForUpdatesAsync(); - expect(trackStart?.style.top).toBe('0%'); - expect(thumb?.style.top).toBe('0%'); + expect(pageObject.getTrackStartPosition()).toContain('top: 0%'); + expect(pageObject.getThumbPosition()).toContain('top: 0%'); }); it('increases a vertical slider value when dragged upward', async () => { const fixtureResult = await setup( - html`<${sliderTag} orientation="vertical"></${sliderTag}>` + html`<${thSliderTag} orientation="vertical"></${thSliderTag}>` ); const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; await connect(); + pageObject = new ThSliderPageObject(element); spyOn(element.track, 'getBoundingClientRect').and.returnValue( new DOMRect(0, 100, 4, 200) ); - element.dispatchEvent( - new MouseEvent('mousedown', { bubbles: true, clientY: 300 }) - ); - window.dispatchEvent(new MouseEvent('mousemove', { clientY: 100 })); - await waitForUpdatesAsync(); + await pageObject.mouseDown(0, 300); + await pageObject.mouseMove(0, 100); expect(element.value).toBe('10'); }); it('uses up and down arrow keys to increase and decrease a vertical slider', async () => { const fixtureResult = await setup( - html`<${sliderTag} + html`<${thSliderTag} value="5" orientation="vertical" - ></${sliderTag}>` + ></${thSliderTag}>` ); const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; await connect(); + pageObject = new ThSliderPageObject(element); - element.dispatchEvent( - new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }) - ); + await pageObject.pressKey('ArrowUp'); expect(element.value).toBe('6'); - element.dispatchEvent( - new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }) - ); + await pageObject.pressKey('ArrowDown'); expect(element.value).toBe('5'); }); it('sets disabled accessibility attributes and removes tabindex', async () => { const fixtureResult = await setup( - html`<${sliderTag} disabled></${sliderTag}>` + html`<${thSliderTag} disabled></${thSliderTag}>` ); const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; @@ -154,7 +137,7 @@ describe('Slider', () => { it('sets readonly accessibility attributes', async () => { const fixtureResult = await setup( - html`<${sliderTag} readonly></${sliderTag}>` + html`<${thSliderTag} readonly></${thSliderTag}>` ); const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; @@ -166,22 +149,19 @@ describe('Slider', () => { it('can display the current value next to the thumb', async () => { const fixtureResult = await setup( - html`<${sliderTag} value="4" value-visible></${sliderTag}>` + html`<${thSliderTag} value="4" value-visible></${thSliderTag}>` ); const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; await connect(); + pageObject = new ThSliderPageObject(element); expect(element.valueVisible).toBeTrue(); - expect( - element.shadowRoot?.querySelector('.value-label')?.textContent?.trim() - ).toBe('4'); + expect(pageObject.getValueLabelText()).toBe('4'); element.value = '7'; await waitForUpdatesAsync(); - expect( - element.shadowRoot?.querySelector('.value-label')?.textContent?.trim() - ).toBe('7'); + expect(pageObject.getValueLabelText()).toBe('7'); }); it('updates range labels when min and max change', async () => { @@ -189,17 +169,14 @@ describe('Slider', () => { const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; await connect(); + pageObject = new ThSliderPageObject(element); element.min = 3; element.max = 9; await waitForUpdatesAsync(); - expect( - element.shadowRoot?.querySelector('.minimum-label')?.textContent?.trim() - ).toBe('3'); - expect( - element.shadowRoot?.querySelector('.maximum-label')?.textContent?.trim() - ).toBe('9'); + expect(pageObject.getRangeLabelText('minimum-label')).toBe('3'); + expect(pageObject.getRangeLabelText('maximum-label')).toBe('9'); }); it('defaults to showing min and max labels on hover', async () => { @@ -207,37 +184,36 @@ describe('Slider', () => { const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; await connect(); + pageObject = new ThSliderPageObject(element); - expect(element.showMinMax).toBe(SliderShowMinMax.hover); + expect(element.showMinMax).toBe(ThSliderShowMinMax.hover); }); it('can always show min and max labels', async () => { const fixtureResult = await setup( - html`<${sliderTag} show-min-max="always"></${sliderTag}>` + html`<${thSliderTag} show-min-max="always"></${thSliderTag}>` ); const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; await connect(); + pageObject = new ThSliderPageObject(element); - const minimumLabel = element.shadowRoot?.querySelector('.minimum-label'); - const maximumLabel = element.shadowRoot?.querySelector('.maximum-label'); - expect(element.showMinMax).toBe(SliderShowMinMax.always); - expect(getComputedStyle(minimumLabel!).display).toBe('block'); - expect(getComputedStyle(maximumLabel!).display).toBe('block'); + expect(element.showMinMax).toBe(ThSliderShowMinMax.always); + expect(pageObject.getRangeLabelDisplay('minimum-label')).toBe('block'); + expect(pageObject.getRangeLabelDisplay('maximum-label')).toBe('block'); }); it('can never show min and max labels', async () => { const fixtureResult = await setup( - html`<${sliderTag} show-min-max="never"></${sliderTag}>` + html`<${thSliderTag} show-min-max="never"></${thSliderTag}>` ); const { element, connect } = fixtureResult; disconnect = fixtureResult.disconnect; await connect(); + pageObject = new ThSliderPageObject(element); - const minimumLabel = element.shadowRoot?.querySelector('.minimum-label'); - const maximumLabel = element.shadowRoot?.querySelector('.maximum-label'); - expect(element.showMinMax).toBe(SliderShowMinMax.never); - expect(getComputedStyle(minimumLabel!).display).toBe('none'); - expect(getComputedStyle(maximumLabel!).display).toBe('none'); + expect(element.showMinMax).toBe(ThSliderShowMinMax.never); + expect(pageObject.getRangeLabelDisplay('minimum-label')).toBe('none'); + expect(pageObject.getRangeLabelDisplay('maximum-label')).toBe('none'); }); }); diff --git a/packages/ok-components/src/th/slider/types.ts b/packages/ok-components/src/th/slider/types.ts index 03c93914fd..92d6620ffe 100644 --- a/packages/ok-components/src/th/slider/types.ts +++ b/packages/ok-components/src/th/slider/types.ts @@ -1,7 +1,7 @@ -export const SliderShowMinMax = { +export const ThSliderShowMinMax = { always: 'always', hover: 'hover', never: 'never' } as const; -export type SliderShowMinMax = (typeof SliderShowMinMax)[keyof typeof SliderShowMinMax]; \ No newline at end of file +export type ThSliderShowMinMax = (typeof ThSliderShowMinMax)[keyof typeof ThSliderShowMinMax]; \ No newline at end of file diff --git a/packages/react-workspace/ok-react/src/th/slider/index.ts b/packages/react-workspace/ok-react/src/th/slider/index.ts index 97739c4064..7b66c493e7 100644 --- a/packages/react-workspace/ok-react/src/th/slider/index.ts +++ b/packages/react-workspace/ok-react/src/th/slider/index.ts @@ -1,18 +1,18 @@ 'use client'; import { - Slider, - sliderTag + ThSlider, + thSliderTag } from '@ni/ok-components/dist/esm/th/slider'; import { wrap, type EventName } from '../../utilities/react-wrapper'; -export { sliderTag }; -export { type Slider }; -export const OkThSlider = wrap(Slider, { +export { thSliderTag }; +export { type ThSlider }; +export const OkThSlider = wrap(ThSlider, { events: { - onChange: 'change' as EventName<SliderChangeEvent> + onChange: 'change' as EventName<ThSliderChangeEvent> } }); -export interface SliderChangeEvent extends CustomEvent { - target: Slider; +export interface ThSliderChangeEvent extends CustomEvent { + target: ThSlider; } diff --git a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts b/packages/storybook/src/ok/th/slider/th-slider-matrix.stories.ts similarity index 94% rename from packages/storybook/src/ok/th/slider/slider-matrix.stories.ts rename to packages/storybook/src/ok/th/slider/th-slider-matrix.stories.ts index 9716a20622..1721d63324 100644 --- a/packages/storybook/src/ok/th/slider/slider-matrix.stories.ts +++ b/packages/storybook/src/ok/th/slider/th-slider-matrix.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryFn } from '@storybook/html-vite'; import { html, type ViewTemplate } from '@ni/fast-element'; -import { sliderTag } from '@ni/ok-components/dist/esm/th/slider'; +import { thSliderTag } from '@ni/ok-components/dist/esm/th/slider'; import { cartesianProduct, createMatrix, @@ -46,14 +46,14 @@ const component = ( ): ViewTemplate => html` <div style="display: inline-flex; flex-direction: column; gap: 8px; margin: 8px;"> <span style="font-size: 12px; color: #0076d6; text-align: center; min-height: 16px;">${() => `${orientationName} ${stateName}`}</span> - <${sliderTag} + <${thSliderTag} style="${() => getSliderStyle(orientation)}" value="4" orientation="${() => orientation}" ?disabled="${() => disabled}" ?readonly="${() => readOnly}" value-visible - ></${sliderTag}> + ></${thSliderTag}> </div> `; diff --git a/packages/storybook/src/ok/th/slider/slider.mdx b/packages/storybook/src/ok/th/slider/th-slider.mdx similarity index 89% rename from packages/storybook/src/ok/th/slider/slider.mdx rename to packages/storybook/src/ok/th/slider/th-slider.mdx index 26f82d2743..2fea884c72 100644 --- a/packages/storybook/src/ok/th/slider/slider.mdx +++ b/packages/storybook/src/ok/th/slider/th-slider.mdx @@ -1,6 +1,6 @@ import { Canvas, Controls, Meta, Title } from '@storybook/addon-docs/blocks'; import ComponentApisLink from '../../../docs/component-apis-link.mdx'; -import * as sliderStories from './slider.stories'; +import * as sliderStories from './th-slider.stories'; <Meta of={sliderStories} /> <Title of={sliderStories} /> diff --git a/packages/storybook/src/ok/th/slider/slider.stories.ts b/packages/storybook/src/ok/th/slider/th-slider.stories.ts similarity index 92% rename from packages/storybook/src/ok/th/slider/slider.stories.ts rename to packages/storybook/src/ok/th/slider/th-slider.stories.ts index d01f3bc5e6..3c0a7ea993 100644 --- a/packages/storybook/src/ok/th/slider/slider.stories.ts +++ b/packages/storybook/src/ok/th/slider/th-slider.stories.ts @@ -1,9 +1,9 @@ import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html-vite'; import { html } from '@ni/fast-element'; import { - sliderTag, - SliderShowMinMax, - type SliderShowMinMax as SliderShowMinMaxType + thSliderTag, + ThSliderShowMinMax, + type ThSliderShowMinMax as ThSliderShowMinMaxType } from '@ni/ok-components/dist/esm/th/slider'; import { withActions } from 'storybook/actions/decorator'; import { @@ -22,7 +22,7 @@ interface SliderArgs { disabled: boolean; readOnly: boolean; valueVisible: boolean; - showMinMax: SliderShowMinMaxType; + showMinMax: ThSliderShowMinMaxType; change?: (event: Event) => void; } @@ -43,7 +43,7 @@ const metadata: Meta<SliderArgs> = { componentName: 'Th Slider', statusLink: './?path=/docs/component-status--docs#ok-components' })} - <${sliderTag} + <${thSliderTag} style="${x => getSliderStyle(x.orientation)}" value="${x => x.value}" min="${x => x.min}" @@ -54,7 +54,7 @@ const metadata: Meta<SliderArgs> = { ?readonly="${x => x.readOnly}" ?value-visible="${x => x.valueVisible}" show-min-max="${x => x.showMinMax}" - ></${sliderTag}> + ></${thSliderTag}> `), argTypes: { value: { @@ -94,7 +94,7 @@ const metadata: Meta<SliderArgs> = { showMinMax: { name: 'show-min-max', description: 'Controls when the minimum and maximum value labels are displayed.', - options: Object.values(SliderShowMinMax), + options: Object.values(ThSliderShowMinMax), control: { type: 'radio' }, table: { category: apiCategory.attributes } }, @@ -112,7 +112,7 @@ const metadata: Meta<SliderArgs> = { disabled: false, readOnly: false, valueVisible: true, - showMinMax: SliderShowMinMax.hover + showMinMax: ThSliderShowMinMax.hover } };