From 35a3c45ee9b23c683f45df68a2a917cc7c9cc1dd Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Thu, 25 Jan 2024 16:48:08 +0100 Subject: [PATCH] feat: Add `LemonSlider` and use it in flag rollout conditions (#19958) * Add `LemonSlider` * Replace Ant `Slider` with `LemonSlider` * Use `LemonSlider` in feature flags * Fix slider sizing * Update UI snapshots for `chromium` (1) * Fix touch and add ring * Reduce transition duration slightly * Restore utilities.scss * Update UI snapshots for `chromium` (1) * Remove leftover comment --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- .eslintrc.js | 4 + .../lemon-ui-lemon-slider--basic--dark.png | Bin 0 -> 614 bytes .../lemon-ui-lemon-slider--basic--light.png | Bin 0 -> 623 bytes frontend/src/lib/hooks/useEventListener.ts | 18 ++- .../lib/lemon-ui/LemonInput/LemonInput.scss | 6 + .../LemonSlider/LemonSlider.stories.tsx | 17 +++ .../lib/lemon-ui/LemonSlider/LemonSlider.tsx | 104 ++++++++++++++++++ .../src/lib/lemon-ui/LemonSlider/index.ts | 1 + .../src/scenes/experiments/Experiment.scss | 14 --- .../scenes/experiments/ExperimentPreview.tsx | 38 +++---- .../FeatureFlagReleaseConditions.tsx | 15 ++- frontend/src/styles/global.scss | 13 +++ 12 files changed, 191 insertions(+), 39 deletions(-) create mode 100644 frontend/__snapshots__/lemon-ui-lemon-slider--basic--dark.png create mode 100644 frontend/__snapshots__/lemon-ui-lemon-slider--basic--light.png create mode 100644 frontend/src/lib/lemon-ui/LemonSlider/LemonSlider.stories.tsx create mode 100644 frontend/src/lib/lemon-ui/LemonSlider/LemonSlider.tsx create mode 100644 frontend/src/lib/lemon-ui/LemonSlider/index.ts diff --git a/.eslintrc.js b/.eslintrc.js index 2441c77348e49..6ea14dea0d739 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -204,6 +204,10 @@ module.exports = { element: 'Collapse', message: 'use instead', }, + { + element: 'Slider', + message: 'use instead', + }, { element: 'Checkbox', message: 'use instead', diff --git a/frontend/__snapshots__/lemon-ui-lemon-slider--basic--dark.png b/frontend/__snapshots__/lemon-ui-lemon-slider--basic--dark.png new file mode 100644 index 0000000000000000000000000000000000000000..51342d621ca39f677311147bf9ba7074c2f42013 GIT binary patch literal 614 zcmV-s0-61ZP)Px%AW1|)RA_G2q}1@VhBSel1fO4zywp5(6eD8J zh{6o8EDK8~JFMH9(`(OowEGi)kCO`hQJsP3qHRqa#l&%3l$5ooBBF5m=Wja^M+awg z8jF?VhT|%S$9sGWG*K90Vj&dL1k+`|?&ASEjRi*sXNeH4i;%LB+zn7xvcJ2n`Pz?9 z=`W*}d~Cx5B4iDgeudmxXj}7)H72*u*W{rdF$6uOOt@eiyV}zuV!{c{R6! z&3jizT~^YuD>FkuT!bOM?_-~M4kzCI+zKm8p*8eJ^-N6=lCc0m5HOicbJLQA+_!+l z0wN4Nm*IHx${LQV3_Lf9#|ENtAw3|9l4f>zytmebGzk4UXWJB#1mW)}{QU8ZUVBb+ zC+QET(+d5O%i9mh(*N(G@Upl9t@YI{@LxoTqR2=DPARnQiX>-ydL33=-U7KH*Wnb2 z1u<~OXW?1}Arq7am=<7~(g4!}Oj8Px%DM>^@RA_hJ{B>l1}rNU2DY+q$Gw(yAj0 zFW&s^HOcuA*<}3JaiY^DK79k*JEXHaFE_z?5fnl$u6@j>0^DfqC@Q z*@D0R>Q)v}j!n}$XF8T4rK0^{2NfI~!@)6`qlHZv69Ve+gxLy3ka8@5RFd7@$BvY& zhVxIr7!7%j>h%$w?s5U0E~?juJTK<18RrIMnI+W;0P*Rt(u5)i4FO1X;-xRBB?-cB zQFt^cqVVSr0Ej2giYQ!OePi`zMd3L?5D^72wEj`u^PE&Co7oHjXq{rML8-DGE~Bo^ z%68ancsmS5O_ void +export type TouchEventHandler = (event: TouchEvent) => void +export type MouseEventHandler = (event: MouseEvent) => void export type EventHandler = (event: Event) => void export function useEventListener( - eventName: 'keyup' | 'keydown', + eventName: `key${string}`, handler: KeyboardEventHandler, element?: Element | Window | null, deps?: DependencyList ): void +export function useEventListener( + eventName: `touch${string}`, + handler: TouchEventHandler, + element?: Element | Window | null, + deps?: DependencyList +): void +export function useEventListener( + eventName: `mouse${string}`, + handler: MouseEventHandler, + element?: Element | Window | null, + deps?: DependencyList +): void export function useEventListener( eventName: string, handler: EventHandler, @@ -17,7 +31,7 @@ export function useEventListener( ): void export function useEventListener( eventName: string, - handler: EventHandler | KeyboardEventHandler, + handler: KeyboardEventHandler | TouchEventHandler | MouseEventHandler | EventHandler, element: Element | Window | null = window, deps?: DependencyList ): void { diff --git a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss index 21d86a69c33b3..6018b10f41bb7 100644 --- a/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss +++ b/frontend/src/lib/lemon-ui/LemonInput/LemonInput.scss @@ -87,6 +87,12 @@ max-width: 240px; } + &.LemonInput--type-number { + .LemonInput__input { + text-overflow: clip; + } + } + &.LemonInput--full-width { width: 100%; max-width: 100%; diff --git a/frontend/src/lib/lemon-ui/LemonSlider/LemonSlider.stories.tsx b/frontend/src/lib/lemon-ui/LemonSlider/LemonSlider.stories.tsx new file mode 100644 index 0000000000000..7b293618d7ff3 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonSlider/LemonSlider.stories.tsx @@ -0,0 +1,17 @@ +import { Meta } from '@storybook/react' +import { useState } from 'react' + +import { LemonSlider } from './LemonSlider' + +const meta: Meta = { + title: 'Lemon UI/Lemon Slider', + component: LemonSlider, + tags: ['autodocs'], +} +export default meta + +export function Basic(): JSX.Element { + const [value, setValue] = useState(42) + + return +} diff --git a/frontend/src/lib/lemon-ui/LemonSlider/LemonSlider.tsx b/frontend/src/lib/lemon-ui/LemonSlider/LemonSlider.tsx new file mode 100644 index 0000000000000..fe72960e0453d --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonSlider/LemonSlider.tsx @@ -0,0 +1,104 @@ +import clsx from 'clsx' +import { useEventListener } from 'lib/hooks/useEventListener' +import { useRef, useState } from 'react' + +export interface LemonSliderProps { + value?: number + onChange?: (value: number) => void + min: number + max: number + /** @default 1 */ + step?: number + className?: string +} + +export function LemonSlider({ value = 0, onChange, min, max, step = 1, className }: LemonSliderProps): JSX.Element { + const trackRef = useRef(null) + const movementStartValueWithX = useRef<[number, number] | null>(null) + const [dragging, setDragging] = useState(false) + + const handleMove = (clientX: number): void => { + if (trackRef.current && movementStartValueWithX.current !== null) { + const [movementStartValue, movementStartX] = movementStartValueWithX.current + const rect = trackRef.current.getBoundingClientRect() + const adjustedWidth = rect.width - 16 // 16px = handle width + const deltaX = (clientX - movementStartX) / adjustedWidth + let newValue = movementStartValue + (max - min) * deltaX + newValue = Math.max(min, Math.min(max, newValue)) // Clamped + if (step !== undefined) { + newValue = Math.round(newValue / step) * step // Adjusted to step + } + onChange?.(newValue) + } + } + useEventListener('mousemove', (e) => { + handleMove(e.clientX) + }) + useEventListener('touchmove', (e) => { + if (e.touches.length === 1) { + handleMove(e.touches[0].clientX) + } + }) + + useEventListener('mouseup', (e) => { + if (e.button === 0) { + movementStartValueWithX.current = null + setDragging(false) + } + }) + useEventListener('touchend', () => { + movementStartValueWithX.current = null + setDragging(false) + }) + useEventListener('touchcancel', () => { + movementStartValueWithX.current = null + setDragging(false) + }) + + const proportion = Math.round(((value - min) / (max - min)) * 100) / 100 + + return ( +
+
{ + const rect = e.currentTarget.getBoundingClientRect() + const x = e.clientX - (rect.left + 8) // 4px = half the handle + const adjustedWidth = rect.width - 16 // 8px = handle width + let newValue = (x / adjustedWidth) * (max - min) + min + newValue = Math.max(min, Math.min(max, newValue)) // Clamped + if (step !== undefined) { + newValue = Math.round(newValue / step) * step // Adjusted to step + } + onChange?.(newValue) + }} + > +
+
+
+
{ + movementStartValueWithX.current = [value, e.clientX] + setDragging(true) + }} + onTouchStart={(e) => { + movementStartValueWithX.current = [value, e.touches[0].clientX] + setDragging(true) + }} + /> +
+ ) +} diff --git a/frontend/src/lib/lemon-ui/LemonSlider/index.ts b/frontend/src/lib/lemon-ui/LemonSlider/index.ts new file mode 100644 index 0000000000000..3d860c8215357 --- /dev/null +++ b/frontend/src/lib/lemon-ui/LemonSlider/index.ts @@ -0,0 +1 @@ +export { LemonSlider, type LemonSliderProps } from './LemonSlider' diff --git a/frontend/src/scenes/experiments/Experiment.scss b/frontend/src/scenes/experiments/Experiment.scss index 633259b80037c..53f59ba971b26 100644 --- a/frontend/src/scenes/experiments/Experiment.scss +++ b/frontend/src/scenes/experiments/Experiment.scss @@ -16,20 +16,6 @@ .experiment-preview { margin-bottom: 1rem; border-bottom: 1px solid var(--border); - - .mde-slider { - .ant-slider-handle { - border: none; - } - - .ant-slider-rail { - background-color: var(--primary-3000-highlight); - } - - .ant-slider-handle:focus { - box-shadow: 0 0 0 5px var(--primary-3000-highlight); - } - } } .variants { diff --git a/frontend/src/scenes/experiments/ExperimentPreview.tsx b/frontend/src/scenes/experiments/ExperimentPreview.tsx index 56275dd7fb31d..2066d8d73709c 100644 --- a/frontend/src/scenes/experiments/ExperimentPreview.tsx +++ b/frontend/src/scenes/experiments/ExperimentPreview.tsx @@ -1,5 +1,4 @@ import { LemonButton, LemonDivider, LemonInput, LemonModal, Tooltip } from '@posthog/lemon-ui' -import { Slider } from 'antd' import { useActions, useValues } from 'kea' import { Field, Form } from 'kea-forms' import { InsightLabel } from 'lib/components/InsightLabel' @@ -7,6 +6,7 @@ import { PropertyFilterButton } from 'lib/components/PropertyFilters/components/ import { TZLabel } from 'lib/components/TZLabel' import { dayjs } from 'lib/dayjs' import { IconInfo } from 'lib/lemon-ui/icons' +import { LemonSlider } from 'lib/lemon-ui/LemonSlider' import { humanFriendlyNumber } from 'lib/utils' import { groupFilters } from 'scenes/feature-flags/FeatureFlags' import { urls } from 'scenes/urls' @@ -112,26 +112,22 @@ export function ExperimentPreview({
-
-
- { - setExperiment({ - parameters: { - ...experiment.parameters, - minimum_detectable_effect: value, - }, - }) - }} - tipFormatter={(value) => `${value}%`} - /> -
+
+ { + setExperiment({ + parameters: { + ...experiment.parameters, + minimum_detectable_effect: value, + }, + }) + }} + className="w-1/3" + />
Roll out to{' '} + { + updateConditionSet(index, value) + }} + min={0} + max={100} + step={1} + className="ml-1.5 w-20" + /> { updateConditionSet(index, value === undefined ? 0 : value) }} - value={group.rollout_percentage != null ? group.rollout_percentage : 100} + value={group.rollout_percentage ?? 100} min={0} max={100} step="any" diff --git a/frontend/src/styles/global.scss b/frontend/src/styles/global.scss index f830326306f98..f3d49b5da9388 100644 --- a/frontend/src/styles/global.scss +++ b/frontend/src/styles/global.scss @@ -478,6 +478,19 @@ body { --tooltip-bg: var(--bg-charcoal); --data-color-1-hover: #1d4affe5; + // Remove below once we're using Tailwind's base + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: var(--bg-light); + --tw-ring-color: var(--primary-3000); + --tw-ring-inset: ; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + touch-action: manipulation; // Disable double-tap-to-zoom on mobile, making taps slightly snappier &.posthog-3000[theme='light'] {