Repository: streamlit/streamlit

Issue:
datetimes that are close together on silder used for ranges are illegible.
### Steps to reproduce

Here's an example:

```python

from datetime import date
import streamlit as st

dates = st.slider('date range',
                  min_value=date(2019, 8, 1), max_value=date(2021, 6, 4),
                  value=(date(2021, 5, 21), date(2021, 6, 4)))
```

**Expected behavior:**

Legible dates and handles on the slider.

**Actual behavior:**

![image](https://user-images.githubusercontent.com/632049/121186310-dcb0fe00-c85e-11eb-8b88-f201d69d51f3.png)

### Is this a regression?

 no

### Debug info

- Streamlit version: Streamlit, version 0.81.1
- Python version: Python 3.9.4
- Using Conda? yes
- OS version: Windows 10
- Browser version: Latest Chrome

### Additional information

It probably makes sense to right-align the lower end of the range and left-align the upper end of the range.


Discussion:
Thanks for the feedback here @cjw296 ! I will put this in our backlog!
Had conversation with PM about this issue and we should try stacking the dates when they are overlapping as a solution. Recording conversation here in case someone else picks up this issue. 
Hey, happy to work on this as I already did it for a project - should they be stacked both above the slider or one above, one below as on screen?

![obraz](https://user-images.githubusercontent.com/11256405/225852069-4afcad1a-68cf-4f85-b5b7-294380155a7f.png)

Hm, CCing @jrieke to get an opinion on this. I think I'd prefer one above/one below as shown in the screenshot, but a member of the product team should probably make the call.
Oops, sorry for the delay! Just saw this now. I think my preference would actually be to do it like this:

![CleanShot 2023-04-05 at 01 08 33](https://user-images.githubusercontent.com/5103165/229941994-4dd264b8-37e2-47d5-a1b9-5ea6edc0fa99.png)

I.e. just have the left date offset a bit to the left, so it doesn't overlap the right date. With stacking vertically, I think we'll run into trouble quickly. If we stack both dates above the slider, they are so high they they'll overlap the widget label on the left side. If we stack one above/one below, then it looks very cramped and it's also a bit confusing which date is which. 
Why do we need moving values we could have visually fixed min max values below the slider and fixed min max selection values above the slider. No moving or realigning needed.

Resolving patch:
diff --git a/e2e/scripts/st_slider.py b/e2e/scripts/st_slider.py
--- a/e2e/scripts/st_slider.py
+++ b/e2e/scripts/st_slider.py
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from datetime import date
+
 import streamlit as st
 from streamlit import runtime
 
@@ -52,19 +54,27 @@
 w6 = st.slider("Label 6", 0, 100, 36, label_visibility="collapsed")
 st.write("Value 6:", w6)
 
+dates = st.slider(
+    "Label 7",
+    min_value=date(2019, 8, 1),
+    max_value=date(2021, 6, 4),
+    value=(date(2019, 8, 1), date(2019, 9, 1)),
+)
+st.write("Value 7:", dates[0], dates[1])
+
 if runtime.exists():
 
     def on_change():
         st.session_state.slider_changed = True
 
     st.slider(
-        "Label 7",
+        "Label 8",
         min_value=0,
         max_value=100,
         value=25,
         step=1,
-        key="slider7",
+        key="slider8",
         on_change=on_change,
     )
-    st.write("Value 7:", st.session_state.slider7)
+    st.write("Value 8:", st.session_state.slider8)
     st.write("Slider changed:", "slider_changed" in st.session_state)


Test patch:
diff --git a/frontend/lib/src/components/widgets/Slider/Slider.test.tsx b/frontend/lib/src/components/widgets/Slider/Slider.test.tsx
--- a/frontend/lib/src/components/widgets/Slider/Slider.test.tsx
+++ b/frontend/lib/src/components/widgets/Slider/Slider.test.tsx
@@ -15,19 +15,25 @@
  */
 
 import React from "react"
+import "@testing-library/jest-dom"
 import { Slider as UISlider } from "baseui/slider"
+import { screen } from "@testing-library/react"
+
 import TimezoneMock from "timezone-mock"
 
 import {
   LabelVisibilityMessage as LabelVisibilityMessageProto,
   Slider as SliderProto,
 } from "@streamlit/lib/src/proto"
-import { mount } from "@streamlit/lib/src/test_util"
+import { render, mount } from "@streamlit/lib/src/test_util"
 import { WidgetStateManager } from "@streamlit/lib/src/WidgetStateManager"
 import { mockTheme } from "@streamlit/lib/src/mocks/mockTheme"
 import Slider, { Props } from "./Slider"
 
-const getProps = (elementProps: Partial<SliderProto> = {}): Props => ({
+const getProps = (
+  elementProps: Partial<SliderProto> = {},
+  props: Partial<Props> = {}
+): Props => ({
   element: SliderProto.create({
     id: "1",
     label: "Label",
@@ -39,13 +45,14 @@ const getProps = (elementProps: Partial<SliderProto> = {}): Props => ({
     options: [],
     ...elementProps,
   }),
-  width: 0,
+  width: 600,
   disabled: false,
   widgetMgr: new WidgetStateManager({
     sendRerunBackMsg: jest.fn(),
     formsDataChanged: jest.fn(),
   }),
   theme: mockTheme.emotion,
+  ...props,
 })
 
 describe("Slider widget", () => {
@@ -58,9 +65,10 @@ describe("Slider widget", () => {
 
   it("shows a label", () => {
     const props = getProps()
-    const wrapper = mount(<Slider {...props} />)
+    render(<Slider {...props} />)
 
-    expect(wrapper.find("StyledWidgetLabel").text()).toBe("Label")
+    const widgetLabel = screen.queryByText(`${props.element.label}`)
+    expect(widgetLabel).toBeInTheDocument()
   })
 
   it("pass labelVisibility prop to StyledWidgetLabel correctly when hidden", () => {
@@ -69,9 +77,9 @@ describe("Slider widget", () => {
         value: LabelVisibilityMessageProto.LabelVisibilityOptions.HIDDEN,
       },
     })
-    const wrapper = mount(<Slider {...props} />)
-    expect(wrapper.find("StyledWidgetLabel").prop("labelVisibility")).toEqual(
-      LabelVisibilityMessageProto.LabelVisibilityOptions.HIDDEN
+    render(<Slider {...props} />)
+    expect(screen.getByTestId("stWidgetLabel")).toHaveStyle(
+      "visibility: hidden"
     )
   })
 
@@ -81,69 +89,63 @@ describe("Slider widget", () => {
         value: LabelVisibilityMessageProto.LabelVisibilityOptions.COLLAPSED,
       },
     })
-    const wrapper = mount(<Slider {...props} />)
-    expect(wrapper.find("StyledWidgetLabel").prop("labelVisibility")).toEqual(
-      LabelVisibilityMessageProto.LabelVisibilityOptions.COLLAPSED
-    )
+    render(<Slider {...props} />)
+    expect(screen.getByTestId("stWidgetLabel")).toHaveStyle("display: none")
   })
 
   it("sets widget value on mount", async () => {
     const props = getProps()
     jest.spyOn(props.widgetMgr, "setDoubleArrayValue")
 
-    const wrapper = mount(<Slider {...props} />)
+    render(<Slider {...props} />)
 
     // We need to do this as we are using a debounce when the widget value is set
     jest.runAllTimers()
-    wrapper.update()
 
     expect(props.widgetMgr.setDoubleArrayValue).toHaveBeenCalledWith(
       props.element,
       [5],
       { fromUi: false }
     )
-
-    wrapper.unmount()
   })
 
-  describe("Overrides", () => {
+  it("renders tick bar with min and max", () => {
     const props = getProps()
-    const wrapper = mount(<Slider {...props} />)
-
-    it("renders tick bar with min and max", () => {
-      expect(
-        wrapper.find("StyledTickBarItem[data-testid='stTickBarMin']").text()
-      ).toBe("0")
-      expect(
-        wrapper.find("StyledTickBarItem[data-testid='stTickBarMax']").text()
-      ).toBe("10")
-    })
+    render(<Slider {...props} />)
+
+    const min = screen.getByTestId("stTickBarMin")
+    const max = screen.getByTestId("stTickBarMax")
+    expect(min).toHaveTextContent("0")
+    expect(max).toHaveTextContent("10")
   })
 
   describe("Single value", () => {
     it("renders without crashing", () => {
       const props = getProps()
-      const wrapper = mount(<Slider {...props} />)
+      render(<Slider {...props} />)
 
-      expect(wrapper).toBeDefined()
+      const slider = screen.getByRole("slider")
+      expect(slider).toBeInTheDocument()
     })
 
     it("displays a thumb value", () => {
       const props = getProps()
-      const wrapper = mount(<Slider {...props} />)
+      render(<Slider {...props} />)
 
-      expect(wrapper.find("StyledThumbValue")).toHaveLength(1)
+      expect(screen.getAllByTestId("stThumbValue")).toHaveLength(1)
     })
 
     it("has the correct value", () => {
       const props = getProps()
-      const wrapper = mount(<Slider {...props} />)
-      const UISliderWrapper = wrapper.find(UISlider)
-      const propValue = UISliderWrapper.prop("value")
+      render(<Slider {...props} />)
 
-      expect(propValue).toStrictEqual(props.element.default)
-      expect(propValue[0]).toBeGreaterThanOrEqual(props.element.min)
-      expect(propValue[0]).toBeLessThan(props.element.max)
+      const slider = screen.getByRole("slider")
+      expect(slider).toHaveAttribute(
+        "aria-valuetext",
+        `${props.element.default}`
+      )
+      expect(slider).toHaveAttribute("aria-valuemin", `${props.element.min}`)
+      expect(slider).toHaveAttribute("aria-valuemax", `${props.element.max}`)
     })
 
     it("handles value changes", async () => {
@@ -213,30 +215,51 @@ describe("Slider widget", () => {
   describe("Range value", () => {
     it("renders without crashing", () => {
       const props = getProps({ default: [1, 9] })
-      const wrapper = mount(<Slider {...props} />)
+      render(<Slider {...props} />)
 
-      expect(wrapper).toBeDefined()
+      const sliders = screen.getAllByRole("slider")
+      expect(sliders).toHaveLength(2)
     })
 
     it("displays 2 thumb values", () => {
       const props = getProps({ default: [1, 9] })
-      const wrapper = mount(<Slider {...props} />)
+      render(<Slider {...props} />)
 
-      expect(wrapper.find("StyledThumbValue")).toHaveLength(2)
+      expect(screen.getAllByTestId("stThumbValue")).toHaveLength(2)
     })
 
     it("has the correct value", () => {
       const props = getProps({ default: [1, 9] })
-      const wrapper = mount(<Slider {...props} />)
-      const UISliderWrapper = wrapper.find(UISlider)
-      const propValue = UISliderWrapper.prop("value")
+      render(<Slider {...props} />)
 
-      expect(propValue).toStrictEqual(props.element.default)
+      const sliders = screen.getAllByRole("slider")
+      // First slider - max is the current value of second slider
+      expect(sliders[0]).toHaveAttribute(
+        "aria-valuetext",
+        `${props.element.default[0]}`
+      )
+      expect(sliders[0]).toHaveAttribute(
+        "aria-valuemin",
+        `${props.element.min}`
+      )
+      expect(sliders[0]).toHaveAttribute(
+        "aria-valuemax",
+        `${props.element.default[1]}`
+      )
 
-      propValue.forEach(value => {
-        expect(value).toBeGreaterThanOrEqual(props.element.min)
-        expect(value).toBeLessThan(props.element.max)
-      })
+      // Second slider - min is the current value of first slider
+      expect(sliders[1]).toHaveAttribute(
+        "aria-valuetext",
+        `${props.element.default[1]}`
+      )
+      expect(sliders[1]).toHaveAttribute(
+        "aria-valuemin",
+        `${props.element.default[0]}`
+      )
+      expect(sliders[1]).toHaveAttribute(
+        "aria-valuemax",
+        `${props.element.max}`
+      )
     })
 
     describe("value should be within bounds", () => {
@@ -329,28 +352,27 @@ describe("Slider widget", () => {
       expect(new Date().getTimezoneOffset() === 0).toBeTruthy()
     })
 
-    const DAYS_IN_MICROS = 24 * 60 * 60 * 1000 * 1000
-    const WEEK_IN_MICROS = 7 * DAYS_IN_MICROS
+    it("formats min and max as dates", () => {
+      const DAYS_IN_MICROS = 24 * 60 * 60 * 1000 * 1000
+      const WEEK_IN_MICROS = 7 * DAYS_IN_MICROS
 
-    const props = getProps({
-      // The default value should be divisible by step.
-      // Otherwise, we get a warning from `react-range`.
-      default: [0],
-      min: 0,
-      max: 4 * WEEK_IN_MICROS,
-      step: DAYS_IN_MICROS,
-      format: "YYYY-MM-DD",
-      dataType: SliderProto.DataType.DATETIME,
-    })
-    const wrapper = mount(<Slider {...props} />)
+      const props = getProps({
+        // The default value should be divisible by step.
+        // Otherwise, we get a warning from `react-range`.
+        default: [0],
+        min: 0,
+        max: 4 * WEEK_IN_MICROS,
+        step: DAYS_IN_MICROS,
+        format: "YYYY-MM-DD",
+        dataType: SliderProto.DataType.DATETIME,
+      })
+      render(<Slider {...props} />)
 
-    it("formats min and max as dates", () => {
-      expect(
-        wrapper.find("StyledTickBarItem[data-testid='stTickBarMin']").text()
-      ).toBe("1970-01-01")
-      expect(
-        wrapper.find("StyledTickBarItem[data-testid='stTickBarMax']").text()
-      ).toBe("1970-01-29")
+      const min = screen.getByTestId("stTickBarMin")
+      const max = screen.getByTestId("stTickBarMax")
+
+      expect(min).toHaveTextContent("1970-01-01")
+      expect(max).toHaveTextContent("1970-01-29")
     })
   })
 
@@ -371,9 +393,9 @@ describe("Slider widget", () => {
           "violet",
         ],
       })
-      const wrapper = mount(<Slider {...props} />)
+      render(<Slider {...props} />)
 
-      expect(wrapper).toBeDefined()
+      expect(screen.getByRole("slider")).toBeDefined()
     })
 
     it("sets aria-valuetext correctly", () => {
@@ -392,13 +414,9 @@ describe("Slider widget", () => {
           "violet",
         ],
       })
-      const wrapper = mount(<Slider {...props} />)
-      const sliderDOMNodes = wrapper.find("div[role='slider']")
-      sliderDOMNodes.forEach(node => {
-        expect(node.getDOMNode().getAttribute("aria-valuetext")).toEqual(
-          "orange"
-        )
-      })
+      render(<Slider {...props} />)
+      const slider = screen.getByRole("slider")
+      expect(slider).toHaveAttribute("aria-valuetext", "orange")
     })
 
     it("updates aria-valuetext correctly", () => {
@@ -450,13 +468,10 @@ describe("Slider widget", () => {
           "violet",
         ],
       })
-      const wrapper = mount(<Slider {...props} />)
-      const sliderDOMNodes = wrapper.find("div[role='slider']")
-      const ariaTexts = sliderDOMNodes.map(node =>
-        node.getDOMNode().getAttribute("aria-valuetext")
-      )
-
-      expect(ariaTexts).toEqual(["orange", "blue"])
+      render(<Slider {...props} />)
+      const sliders = screen.getAllByRole("slider")
+      expect(sliders[0]).toHaveAttribute("aria-valuetext", "orange")
+      expect(sliders[1]).toHaveAttribute("aria-valuetext", "blue")
     })
   })
 })