Repository: Unleash/unleash
Pull request #9960: feat: add input box for gradual rollout slider
Author: login: sjaanus
Created: 2025-05-12T11:45:51Z | Merged: 2025-05-12T12:27:31Z

Add input box for gradual rollout slider.
Was not able to get it aligned nicely in the middle of horizontal slider, so its just inline with the row for now. Yes, just adding random padding did work, but that felt too artificial.

This is also behind feature flag. We can improve small CSS stylings in future iterations.

Added % also for the input, as it felt more correct.

![image](https://github.com/user-attachments/assets/956dac13-340e-442f-9eed-d89c1072503d)



Diff:
diff --git a/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx b/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx
index 1f1b956c2f..9236e15835 100644
--- a/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx
+++ b/frontend/src/component/feature/StrategyTypes/FlexibleStrategy/FlexibleStrategy.tsx
@@ -1,7 +1,7 @@
 import { useEffect, useMemo } from 'react';
 import { Box, styled } from '@mui/material';
 import type { IFeatureStrategyParameters } from 'interfaces/strategy';
-import RolloutSlider from '../RolloutSlider/RolloutSlider';
+import ConditionalRolloutSlider from '../RolloutSlider/ConditionalRolloutSlider';
 import Input from 'component/common/Input/Input';
 import {
     FLEXIBLE_STRATEGY_GROUP_ID,
@@ -100,7 +100,7 @@ const FlexibleStrategy = ({
 
     return (
         <StyledBox>
-            <RolloutSlider
+            <ConditionalRolloutSlider
                 name='Rollout'
                 value={rollout}
                 disabled={!editable}
diff --git a/frontend/src/component/feature/StrategyTypes/RolloutSlider/ConditionalRolloutSlider.tsx b/frontend/src/component/feature/StrategyTypes/RolloutSlider/ConditionalRolloutSlider.tsx
new file mode 100644
index 0000000000..22749908fc
--- /dev/null
+++ b/frontend/src/component/feature/StrategyTypes/RolloutSlider/ConditionalRolloutSlider.tsx
@@ -0,0 +1,24 @@
+import RolloutSlider from './RolloutSlider';
+import LegacyRolloutSlider from './LegacyRolloutSlider';
+import { useUiFlag } from 'hooks/useUiFlag';
+
+interface IRolloutSliderProps {
+    name: string;
+    minLabel?: string;
+    maxLabel?: string;
+    value: number;
+    onChange: (e: Event, newValue: number | number[]) => void;
+    disabled?: boolean;
+}
+
+const ConditionalRolloutSlider = (props: IRolloutSliderProps) => {
+    const addEditStrategy = useUiFlag('addEditStrategy');
+
+    if (addEditStrategy) {
+        return <RolloutSlider {...props} />;
+    }
+
+    return <LegacyRolloutSlider {...props} />;
+};
+
+export default ConditionalRolloutSlider;
diff --git a/frontend/src/component/feature/StrategyTypes/RolloutSlider/LegacyRolloutSlider.tsx b/frontend/src/component/feature/StrategyTypes/RolloutSlider/LegacyRolloutSlider.tsx
new file mode 100644
index 0000000000..f195734d35
--- /dev/null
+++ b/frontend/src/component/feature/StrategyTypes/RolloutSlider/LegacyRolloutSlider.tsx
@@ -0,0 +1,162 @@
+import { makeStyles, withStyles } from 'tss-react/mui';
+import { Slider, Typography, Box, styled } from '@mui/material';
+import { ROLLOUT_SLIDER_ID } from 'utils/testIds';
+import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
+
+const StyledSlider = withStyles(Slider, (theme) => ({
+    root: {
+        height: 8,
+    },
+    thumb: {
+        height: 24,
+        width: 24,
+        backgroundColor: theme.palette.background.paper,
+        border: '2px solid currentColor',
+    },
+    active: {},
+    valueLabel: {},
+    track: {
+        height: 8,
+        borderRadius: theme.shape.borderRadius,
+    },
+    rail: {
+        height: 8,
+        borderRadius: theme.shape.borderRadius,
+    },
+}));
+
+const StyledHeader = styled(Typography)(({ theme }) => ({
+    marginBottom: theme.spacing(1),
+}));
+
+const StyledSubheader = styled(Typography)(({ theme }) => ({
+    marginBottom: theme.spacing(1),
+    marginTop: theme.spacing(1),
+}));
+
+const StyledBox = styled(Box)(({ theme }) => ({
+    display: 'flex',
+    alignItems: 'center',
+    marginBottom: theme.spacing(1),
+}));
+
+const useStyles = makeStyles()((theme) => ({
+    slider: {
+        width: '100%',
+        maxWidth: '100%',
+    },
+    margin: {
+        height: theme.spacing(3),
+    },
+}));
+
+const marks = [
+    {
+        value: 0,
+        label: '0%',
+    },
+    {
+        value: 25,
+        label: '25%',
+    },
+    {
+        value: 50,
+        label: '50%',
+    },
+    {
+        value: 75,
+        label: '75%',
+    },
+    {
+        value: 100,
+        label: '100%',
+    },
+];
+
+interface IRolloutSliderProps {
+    name: string;
+    minLabel?: string;
+    maxLabel?: string;
+    value: number;
+    onChange: (e: Event, newValue: number | number[]) => void;
+    disabled?: boolean;
+}
+
+const LegacyRolloutSlider = ({
+    name,
+    value,
+    onChange,
+    disabled = false,
+}: IRolloutSliderProps) => {
+    const { classes } = useStyles();
+
+    const valuetext = (value: number) => `${value}%`;
+
+    return (
+        <div className={classes.slider}>
+            <StyledBox>
+                <Typography id='discrete-slider-always'>{name}</Typography>
+                <HelpIcon
+                    htmlTooltip
+                    tooltip={
+                        <Box>
+                            <StyledHeader variant='h3'>
+                                Rollout percentage
+                            </StyledHeader>
+                            <Typography variant='body2'>
+                                The rollout percentage determines the proportion
+                                of users exposed to a feature. It's based on the
+                                MurmurHash of a user's unique identifier,
+                                normalized to a number between 1 and 100. If the
+                                normalized hash is less than or equal to the
+                                rollout percentage, the user sees the feature.
+                                This ensures a consistent, random distribution
+                                of the feature among users.
+                            </Typography>
+
+                            <StyledSubheader variant='h3'>
+                                Stickiness
+                            </StyledSubheader>
+                            <Typography variant='body2'>
+                                Stickiness refers to the value used for hashing
+                                to ensure a consistent user experience. It
+                                determines the input for the MurmurHash,
+                                ensuring that a user's feature exposure remains
+                                consistent across sessions.
+                                <br />
+                                By default Unleash will use the first value
+                                present in the context in the order of{' '}
+                                <b>userId, sessionId and random</b>.
+                            </Typography>
+
+                            <StyledSubheader variant='h3'>
+                                GroupId
+                            </StyledSubheader>
+                            <Typography variant='body2'>
+                                The groupId is used as a seed for the hash
+                                function, ensuring consistent feature exposure
+                                across different feature flags for a uniform
+                                user experience.
+                            </Typography>
+                        </Box>
+                    }
+                />
+            </StyledBox>
+            <StyledSlider
+                min={0}
+                max={100}
+                value={value}
+                getAriaValueText={valuetext}
+                aria-labelledby='discrete-slider-always'
+                step={1}
+                data-testid={ROLLOUT_SLIDER_ID}
+                marks={marks}
+                onChange={onChange}
+                valueLabelDisplay='on'
+                disabled={disabled}
+            />
+        </div>
+    );
+};
+
+export default LegacyRolloutSlider;
diff --git a/frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx b/frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx
index 87da7478f7..3860da6ac3 100644
--- a/frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx
+++ b/frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx
@@ -1,7 +1,15 @@
-import { makeStyles, withStyles } from 'tss-react/mui';
-import { Slider, Typography, Box, styled } from '@mui/material';
+import { withStyles } from 'tss-react/mui';
+import {
+    Slider,
+    Typography,
+    Box,
+    styled,
+    TextField,
+    InputAdornment,
+} from '@mui/material';
 import { ROLLOUT_SLIDER_ID } from 'utils/testIds';
 import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
+import { useState, useEffect } from 'react';
 
 const StyledSlider = withStyles(Slider, (theme) => ({
     root: {
@@ -37,17 +45,38 @@ const StyledSubheader = styled(Typography)(({ theme }) => ({
 const StyledBox = styled(Box)(({ theme }) => ({
     display: 'flex',
     alignItems: 'center',
+    marginBottom: theme.spacing(2),
+}));
+
+const StyledSliderContainer = styled(Box)(({ theme }) => ({
+    display: 'flex',
+    alignItems: 'center',
+    width: '100%',
+    gap: theme.spacing(4),
     marginBottom: theme.spacing(1),
 }));
 
-const useStyles = makeStyles()((theme) => ({
-    slider: {
-        width: '100%',
-        maxWidth: '100%',
-    },
-    margin: {
-        height: theme.spacing(3),
-    },
+const StyledInputBox = styled(Box)(({ theme }) => ({
+    display: 'flex',
+    alignItems: 'center',
+    width: '100px',
+    flexShrink: 0,
+}));
+
+const SliderWrapper = styled('div')(({ theme }) => ({
+    width: '100%',
+    maxWidth: '100%',
+    display: 'flex',
+    flexDirection: 'column',
+    gap: theme.spacing(2),
+}));
+
+const SliderContent = styled('div')(({ theme }) => ({
+    flexGrow: 1,
+}));
+
+const StyledTextField = styled(TextField)(({ theme }) => ({
+    width: '90px',
 }));
 
 const marks = [
@@ -88,12 +117,30 @@ const RolloutSlider = ({
     onChange,
     disabled = false,
 }: IRolloutSliderProps) => {
-    const { classes } = useStyles();
+    const [inputValue, setInputValue] = useState(value.toString());
+
+    useEffect(() => {
+        setInputValue(value.toString());
+    }, [value]);
 
     const valuetext = (value: number) => `${value}%`;
 
+    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        setInputValue(e.target.value);
+    };
+
+    const handleInputBlur = () => {
+        const numValue = Number.parseInt(inputValue, 10);
+        if (!Number.isNaN(numValue) && numValue >= 0 && numValue <= 100) {
+            const event = new Event('input', { bubbles: true });
+            onChange(event, numValue);
+        } else {
+            setInputValue(value.toString());
+        }
+    };
+
     return (
-        <div className={classes.slider}>
+        <SliderWrapper>
             <StyledBox>
                 <Typography id='discrete-slider-always'>{name}</Typography>
                 <HelpIcon
@@ -142,20 +189,47 @@ const RolloutSlider = ({
                     }
                 />
             </StyledBox>
-            <StyledSlider
-                min={0}
-                max={100}
-                value={value}
-                getAriaValueText={valuetext}
-                aria-labelledby='discrete-slider-always'
-                step={1}
-                data-testid={ROLLOUT_SLIDER_ID}
-                marks={marks}
-                onChange={onChange}
-                valueLabelDisplay='on'
-                disabled={disabled}
-            />
-        </div>
+            <StyledSliderContainer>
+                <SliderContent>
+                    <StyledSlider
+                        min={0}
+                        max={100}
+                        value={value}
+                        getAriaValueText={valuetext}
+                        aria-labelledby='discrete-slider-always'
+                        step={1}
+                        data-testid={ROLLOUT_SLIDER_ID}
+                        marks={marks}
+                        onChange={onChange}
+                        valueLabelDisplay='on'
+                        disabled={disabled}
+                    />
+                </SliderContent>
+                <StyledInputBox>
+                    <StyledTextField
+                        size='small'
+                        aria-labelledby='discrete-slider-always'
+                        type='number'
+                        value={inputValue}
+                        onChange={handleInputChange}
+                        onBlur={handleInputBlur}
+                        disabled={disabled}
+                        inputProps={{
+                            min: 0,
+                            max: 100,
+                            step: 1,
+                        }}
+                        InputProps={{
+                            endAdornment: (
+                                <InputAdornment position='end'>
+                                    %
+                                </InputAdornment>
+                            ),
+                        }}
+                    />
+                </StyledInputBox>
+            </StyledSliderContainer>
+        </SliderWrapper>
     );
 };
 
diff --git a/frontend/src/component/feature/StrategyTypes/StrategyParameter/StrategyParameter.tsx b/frontend/src/component/feature/StrategyTypes/StrategyParameter/StrategyParameter.tsx
index 88baa64ac8..bdb69f163e 100644
--- a/frontend/src/component/feature/StrategyTypes/StrategyParameter/StrategyParameter.tsx
+++ b/frontend/src/component/feature/StrategyTypes/StrategyParameter/StrategyParameter.tsx
@@ -1,7 +1,7 @@
 import type React from 'react';
 import { FormControlLabel, Switch, TextField } from '@mui/material';
 import StrategyInputList from '../StrategyInputList/StrategyInputList';
-import RolloutSlider from '../RolloutSlider/RolloutSlider';
+import ConditionalRolloutSlider from '../RolloutSlider/ConditionalRolloutSlider';
 import type {
     IFeatureStrategyParameters,
     IStrategyParameter,
@@ -57,7 +57,7 @@ export const StrategyParameter = ({
     if (type === 'percentage') {
         return (
             <div>
-                <RolloutSlider
+                <ConditionalRolloutSlider
                     name={name}
                     onChange={onChangePercentage}
                     disabled={!editable}
diff --git a/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm/MilestoneStrategy/MilestoneStrategyTypeFlexible.tsx b/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm/MilestoneStrategy/MilestoneStrategyTypeFlexible.tsx
index ce132935d6..732076e120 100644
--- a/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm/MilestoneStrategy/MilestoneStrategyTypeFlexible.tsx
+++ b/frontend/src/component/releases/ReleasePlanTemplate/TemplateForm/MilestoneStrategy/MilestoneStrategyTypeFlexible.tsx
@@ -1,6 +1,6 @@
 import { Box, styled } from '@mui/material';
 import { StickinessSelect } from 'component/feature/StrategyTypes/FlexibleStrategy/StickinessSelect/StickinessSelect';
-import RolloutSlider from 'component/feature/StrategyTypes/RolloutSlider/RolloutSlider';
+import ConditionalRolloutSlider from '../../../../feature/StrategyTypes/RolloutSlider/ConditionalRolloutSlider';
 import type { IFormErrors } from 'hooks/useFormErrors';
 import type { IFeatureStrategyParameters } from 'interfaces/strategy';
 import { useMemo } from 'react';
@@ -79,7 +79,7 @@ export const MilestoneStrategyTypeFlexible = ({
 
     return (
         <StyledBox>
-            <RolloutSlider
+            <ConditionalRolloutSlider
                 name='Rollout'
                 value={rollout}
                 disabled={!editable}
diff --git a/src/lib/features/frontend-api/frontend-api-controller.ts b/src/lib/features/frontend-api/frontend-api-controller.ts
index bc91ca083d..e45d7a56ef 100644
--- a/src/lib/features/frontend-api/frontend-api-controller.ts
+++ b/src/lib/features/frontend-api/frontend-api-controller.ts
@@ -22,7 +22,6 @@ import type { Context } from 'unleash-client';
 import { enrichContextWithIp } from './index';
 import { corsOriginMiddleware } from '../../middleware';
 import NotImplementedError from '../../error/not-implemented-error';
-import NotFoundError from '../../error/notfound-error';
 import rateLimit from 'express-rate-limit';
 import { minutesToMilliseconds } from 'date-fns';
 import metricsHelper from '../../util/metrics-helper';


Reviews:
user: login: thomasheartman
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T11:54:33Z
user: login: sjaanus
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T11:59:24Z
user: login: thomasheartman
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T12:02:21Z

Review comments:
diff_hunk: @@ -104,18 +157,18 @@ const RolloutSlider = ({
                                 Rollout percentage
                             </StyledHeader>
                             <Typography variant='body2'>
-                                The rollout percentage determines the proportion
-                                of users exposed to a feature. It's based on the
-                                MurmurHash of a user's unique identifier,
-                                normalized to a number between 1 and 100. If the
-                                normalized hash is less than or equal to the
-                                rollout percentage, the user sees the feature.
-                                This ensures a consistent, random distribution
-                                of the feature among users.
+                                T2he rollout percentage determines the
path: frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx
user: login: thomasheartman
body: This looks like a typo! Probably  has knock-on effects for the rest of the formatting too 😄 

```suggestion
                                The rollout percentage determines the
```
created_at: 2025-05-12T11:48:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,18 +157,18 @@ const RolloutSlider = ({
                                 Rollout percentage
                             </StyledHeader>
                             <Typography variant='body2'>
-                                The rollout percentage determines the proportion
-                                of users exposed to a feature. It's based on the
-                                MurmurHash of a user's unique identifier,
-                                normalized to a number between 1 and 100. If the
-                                normalized hash is less than or equal to the
-                                rollout percentage, the user sees the feature.
-                                This ensures a consistent, random distribution
-                                of the feature among users.
+                                T2he rollout percentage determines the
+                                proportion of users exposed to a feature. It's
+                                based on the MurmurHash of a user's unique
+                                identifier, normalized to a number between 1 and
+                                100. If the normalized hash is less than or
+                                equal to the rollout percentage, the user sees
+                                the feature. This ensures a consistent, random
+                                distribution of the feature among users.
                             </Typography>
 
                             <StyledSubheader variant='h3'>
-                                Stickiness
+                                Stickiness1
path: frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx
user: login: thomasheartman
body: Uh, what's with the numbers this time around? 😅 

```suggestion
                                Stickiness
```
created_at: 2025-05-12T11:49:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,20 +195,47 @@ const RolloutSlider = ({
                     }
                 />
             </StyledBox>
-            <StyledSlider
-                min={0}
-                max={100}
-                value={value}
-                getAriaValueText={valuetext}
-                aria-labelledby='discrete-slider-always'
-                step={1}
-                data-testid={ROLLOUT_SLIDER_ID}
-                marks={marks}
-                onChange={onChange}
-                valueLabelDisplay='on'
-                disabled={disabled}
-            />
-        </div>
+            <StyledSliderContainer>
+                <SliderContent>
+                    <StyledSlider
+                        min={0}
+                        max={100}
+                        value={value}
+                        getAriaValueText={valuetext}
+                        aria-labelledby='discrete-slider-always'
+                        step={1}
+                        data-testid={ROLLOUT_SLIDER_ID}
+                        marks={marks}
+                        onChange={onChange}
+                        valueLabelDisplay='on'
+                        disabled={disabled}
+                    />
+                </SliderContent>
+                <StyledInputBox>
+                    <StyledTextField
+                        size='small'
+                        type='number'
+                        value={inputValue}
+                        onChange={handleInputChange}
+                        onBlur={handleInputBlur}
+                        onKeyDown={handleKeyDown}
path: frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx
user: login: thomasheartman
body: Why does this one need onBlur and onKeyDown? Shouldn't onChange be enough?
created_at: 2025-05-12T11:49:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,20 +195,47 @@ const RolloutSlider = ({
                     }
                 />
             </StyledBox>
-            <StyledSlider
-                min={0}
-                max={100}
-                value={value}
-                getAriaValueText={valuetext}
-                aria-labelledby='discrete-slider-always'
-                step={1}
-                data-testid={ROLLOUT_SLIDER_ID}
-                marks={marks}
-                onChange={onChange}
-                valueLabelDisplay='on'
-                disabled={disabled}
-            />
-        </div>
+            <StyledSliderContainer>
+                <SliderContent>
+                    <StyledSlider
+                        min={0}
+                        max={100}
+                        value={value}
+                        getAriaValueText={valuetext}
+                        aria-labelledby='discrete-slider-always'
+                        step={1}
+                        data-testid={ROLLOUT_SLIDER_ID}
+                        marks={marks}
+                        onChange={onChange}
+                        valueLabelDisplay='on'
+                        disabled={disabled}
+                    />
+                </SliderContent>
+                <StyledInputBox>
+                    <StyledTextField
+                        size='small'
path: frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx
user: login: thomasheartman
body: This input definitely needs a label. We can reuse the same one we use for the slider, though.

But also, unless that ID is used by something else, I'd suggest replacing it with `react.useId` to guarantee that it's unique even if there's multiple rollout sliders on a page. (don't know if that can happen atm? change requests? but best to be sure anyway.)

```suggestion
                        size='small'
                        aria-labelledby='discrete-slider-always'
```
created_at: 2025-05-12T11:53:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,20 +195,47 @@ const RolloutSlider = ({
                     }
                 />
             </StyledBox>
-            <StyledSlider
-                min={0}
-                max={100}
-                value={value}
-                getAriaValueText={valuetext}
-                aria-labelledby='discrete-slider-always'
-                step={1}
-                data-testid={ROLLOUT_SLIDER_ID}
-                marks={marks}
-                onChange={onChange}
-                valueLabelDisplay='on'
-                disabled={disabled}
-            />
-        </div>
+            <StyledSliderContainer>
+                <SliderContent>
+                    <StyledSlider
+                        min={0}
+                        max={100}
+                        value={value}
+                        getAriaValueText={valuetext}
+                        aria-labelledby='discrete-slider-always'
+                        step={1}
+                        data-testid={ROLLOUT_SLIDER_ID}
+                        marks={marks}
+                        onChange={onChange}
+                        valueLabelDisplay='on'
+                        disabled={disabled}
+                    />
+                </SliderContent>
+                <StyledInputBox>
+                    <StyledTextField
+                        size='small'
+                        type='number'
+                        value={inputValue}
+                        onChange={handleInputChange}
+                        onBlur={handleInputBlur}
+                        onKeyDown={handleKeyDown}
path: frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx
user: login: sjaanus
body: If we used only onChange on the input field, the slider would update immediately with each keystroke. This would create a poor user experience:
As a user types "75", the slider would first jump to "7" and then to "75"
created_at: 2025-05-12T11:59:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,20 +195,47 @@ const RolloutSlider = ({
                     }
                 />
             </StyledBox>
-            <StyledSlider
-                min={0}
-                max={100}
-                value={value}
-                getAriaValueText={valuetext}
-                aria-labelledby='discrete-slider-always'
-                step={1}
-                data-testid={ROLLOUT_SLIDER_ID}
-                marks={marks}
-                onChange={onChange}
-                valueLabelDisplay='on'
-                disabled={disabled}
-            />
-        </div>
+            <StyledSliderContainer>
+                <SliderContent>
+                    <StyledSlider
+                        min={0}
+                        max={100}
+                        value={value}
+                        getAriaValueText={valuetext}
+                        aria-labelledby='discrete-slider-always'
+                        step={1}
+                        data-testid={ROLLOUT_SLIDER_ID}
+                        marks={marks}
+                        onChange={onChange}
+                        valueLabelDisplay='on'
+                        disabled={disabled}
+                    />
+                </SliderContent>
+                <StyledInputBox>
+                    <StyledTextField
+                        size='small'
+                        type='number'
+                        value={inputValue}
+                        onChange={handleInputChange}
+                        onBlur={handleInputBlur}
+                        onKeyDown={handleKeyDown}
path: frontend/src/component/feature/StrategyTypes/RolloutSlider/RolloutSlider.tsx
user: login: thomasheartman
body: Right. I see the logic there. I don't understand why the onKeyDown handler is there, though. Shouldn't enter submit the form (as is standard with input fields)? Not a big deal, but I don't think we need it 🤷🏼 
created_at: 2025-05-12T12:02:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-12T11:45:56Z
body: [vc]: #jSAYqDFYeewHRycYeRUJGWmUAZKbjAYH6+slHhnTwz0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ1bmxlYXNoLWRvY3MiLCJyb290RGlyZWN0b3J5Ijoid2Vic2l0ZSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3VubGVhc2gtdGVhbS91bmxlYXNoLWRvY3MvQ2FoNFFjU2RyZFhmM24yWGJVRDFwc1FNcldlZyIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicHJldmlld1VybCI6InVubGVhc2gtZG9jcy1naXQtaW5wdXQtc2xpZGVyLXVubGVhc2gtdGVhbS52ZXJjZWwuYXBwIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **unleash-docs** | ⬜️ Ignored ([Inspect](https://vercel.com/unleash-team/unleash-docs/Cah4QcSdrdXf3n2XbUD1psQMrWeg)) | [Visit Preview](https://unleash-docs-git-input-slider-unleash-team.vercel.app) |  | May 12, 2025 0:11am |
</details>


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T11:46:01Z
body: @sjaanus, core features have been modified in this pull request. Please review carefully!
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-12T11:46:07Z
body: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>Scanned Files</h2>
None

<!-- dependency-review-pr-comment-marker -->
author_association: CONTRIBUTOR