Skip to content

Commit

Permalink
Merge pull request #351 from RenaudLN/feature/deselectable-radio-and-…
Browse files Browse the repository at this point in the history
…chip-group

Added `deselectable` prop for radio-type components
  • Loading branch information
AnnMarieW authored Oct 14, 2024
2 parents 9526983 + 49969cc commit a17de74
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Note: `Chip` was available before but not documented. If you used `Chip` in dmc<=0.14.5, you’ll now need to add `controlled=True` for it to work properly on its own.
- Added GitHub actions workflow for automated tests on PRs #333 by @BSd3v
- Added new and missing props to the charts #349 by @AnnMarieW
- RadioGroup and ChipGroup (in single mode) now have a `deselectable` argument to allow resetting the radio value #351



Expand Down
6 changes: 6 additions & 0 deletions src/ts/components/core/chip/Chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { BoxProps } from "props/box";
import { DashBaseProps, PersistenceProps } from "props/dash";
import { StylesApiProps } from "props/styles";
import React from "react";
import ChipGroupContext from "./ChipGroupContext"

interface Props
extends BoxProps,
Expand Down Expand Up @@ -59,11 +60,15 @@ const Chip = (props: Props) => {
const onChange = (checked: boolean) => {
setProps({ checked });
};

const { chipOnClick } = React.useContext(ChipGroupContext) || {};

if (controlled) {
return (
<MantineChip
checked={checked}
onChange={onChange}
onClick={chipOnClick}
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
Expand All @@ -78,6 +83,7 @@ const Chip = (props: Props) => {
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
onClick={chipOnClick}
{...others}
>
{children}
Expand Down
14 changes: 13 additions & 1 deletion src/ts/components/core/chip/ChipGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Chip } from "@mantine/core";
import { useDidUpdate } from "@mantine/hooks";
import { DashBaseProps, PersistenceProps } from "props/dash";
import React, { useState } from "react";
import ChipGroupContext from "./ChipGroupContext";

interface Props extends DashBaseProps, PersistenceProps {
/** Determines whether it is allowed to select multiple values, `false` by default */
Expand All @@ -10,6 +11,8 @@ interface Props extends DashBaseProps, PersistenceProps {
value?: string[] | string | null;
/** `Chip` components and any other elements */
children?: React.ReactNode;
/** Allow to deselect Chip in Radio mode */
deselectable?: boolean;
}

/** ChipGroup */
Expand All @@ -22,6 +25,7 @@ const ChipGroup = (props: Props) => {
persisted_props,
persistence_type,
loading_state,
deselectable,
...others
} = props;
const [val, setVal] = useState(value);
Expand All @@ -34,6 +38,12 @@ const ChipGroup = (props: Props) => {
setProps({ value: val });
}, [val]);

const handleChipClick = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.currentTarget.value === value) {
setVal(null);
}
};

return (
<Chip.Group
value={val}
Expand All @@ -43,7 +53,9 @@ const ChipGroup = (props: Props) => {
}
{...others}
>
{children}
<ChipGroupContext.Provider value={{ chipOnClick: deselectable ? handleChipClick : null }}>
{children}
</ChipGroupContext.Provider>
</Chip.Group>
);
};
Expand Down
9 changes: 9 additions & 0 deletions src/ts/components/core/chip/ChipGroupContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { createContext } from "react";

interface ChipGroupContextProps {
chipOnClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
}

const ChipGroupContext = createContext<ChipGroupContextProps | null>(null);

export default ChipGroupContext;
4 changes: 4 additions & 0 deletions src/ts/components/core/radio/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { BoxProps } from "props/box";
import { DashBaseProps, PersistenceProps } from "props/dash";
import { StylesApiProps } from "props/styles";
import React from "react";
import RadioGroupContext from "./RadioGroupContext";

interface Props
extends BoxProps,
Expand Down Expand Up @@ -49,12 +50,15 @@ const Radio = (props: Props) => {
...others
} = props;

const { radioOnClick } = React.useContext(RadioGroupContext) || {};

return (
<MantineRadio
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
onChange={(ev) => setProps({ checked: ev.currentTarget.checked })}
onClick={radioOnClick}
{...others}
/>
);
Expand Down
14 changes: 13 additions & 1 deletion src/ts/components/core/radio/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MantineSize, Radio } from "@mantine/core";
import { DashBaseProps, PersistenceProps } from "props/dash";
import { InputWrapperProps } from "props/input";
import React from "react";
import RadioGroupContext from "./RadioGroupContext";

interface Props extends InputWrapperProps, DashBaseProps, PersistenceProps {
/** `Radio` components and any other elements */
Expand All @@ -16,6 +17,8 @@ interface Props extends InputWrapperProps, DashBaseProps, PersistenceProps {
name?: string;
/** If set, value cannot be changed */
readOnly?: boolean;
/** Allow to deselect Chip in Radio mode */
deselectable?: boolean;
}

/** RadioGroup */
Expand All @@ -28,13 +31,20 @@ const RadioGroup = (props: Props) => {
persistence,
persisted_props,
persistence_type,
deselectable,
...others
} = props;

const onChange = (value: string) => {
setProps({ value });
};

const handleRadioClick = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.currentTarget.value === value) {
setProps({ value: null });
}
};

return (
<Radio.Group
data-dash-is-loading={
Expand All @@ -44,7 +54,9 @@ const RadioGroup = (props: Props) => {
value={value}
{...others}
>
{children}
<RadioGroupContext.Provider value={{radioOnClick: deselectable ? handleRadioClick : null}}>
{children}
</RadioGroupContext.Provider>
</Radio.Group>
);
};
Expand Down
9 changes: 9 additions & 0 deletions src/ts/components/core/radio/RadioGroupContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { createContext } from "react";

interface RadioGroupContextProps {
radioOnClick?: (e: React.MouseEvent<HTMLInputElement>) => void;
}

const RadioGroupContext = createContext<RadioGroupContextProps | null>(null);

export default RadioGroupContext;
106 changes: 106 additions & 0 deletions tests/test_chip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from dash import Dash, html, Output, Input, _dash_renderer
import dash_mantine_components as dmc

_dash_renderer._set_react_version("18.2.0")


def chipgroup_app(**kwargs):
app = Dash(__name__)

app.layout = dmc.MantineProvider(
html.Div([
dmc.ChipGroup(
id="chip-group",
children=dmc.Group([
dmc.Chip(value="option1", children="Option 1"),
dmc.Chip(value="option2", children="Option 2"),
dmc.Chip(value="option3", children="Option 3"),
]),
**kwargs,
),
html.Div(id="output")
])
)

@app.callback(
Output("output", "children"),
Input("chip-group", "value")
)
def update_output(selected_values):
return f'Selected: {selected_values}'

return app


def test_001ch_chip_group(dash_duo):

app = chipgroup_app()
dash_duo.start_server(app)

# Wait for the app to load
dash_duo.wait_for_element("div.mantine-Group-root")

option1 = dash_duo.find_element("input[value='option1']").find_element_by_xpath("./..")
option2 = dash_duo.find_element("input[value='option2']").find_element_by_xpath("./..")
option1.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: option1")
option2.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: option2")
# Not deselectable
option2.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: option2")

assert dash_duo.get_logs() == []


def test_002ch_chip_group_deselectable(dash_duo):

app = chipgroup_app(deselectable=True)
dash_duo.start_server(app)

# Wait for the app to load
dash_duo.wait_for_element("div.mantine-Group-root")

option1 = dash_duo.find_element("input[value='option1']").find_element_by_xpath("./..")
option1.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: option1")
option1.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: None")

assert dash_duo.get_logs() == []


def test_003ch_chip_group_deselectable_multiple(dash_duo):

app = chipgroup_app(deselectable=True, multiple=True)
dash_duo.start_server(app)

# Wait for the app to load
dash_duo.wait_for_element("div.mantine-Group-root")

option1 = dash_duo.find_element("input[value='option1']").find_element_by_xpath("./..")
option2 = dash_duo.find_element("input[value='option2']").find_element_by_xpath("./..")
option1.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: ['option1']")
option2.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: ['option1', 'option2']")
option2.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: ['option1']")
option1.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: []")

assert dash_duo.get_logs() == []


def test_004ch_single_chip(dash_duo):

app = Dash(__name__)

app.layout = dmc.MantineProvider(dmc.Chip("Radio a", value="a", id="a"))

dash_duo.start_server(app)

# Wait for the app to load
dash_duo.wait_for_element("#a")

assert dash_duo.get_logs() == []
84 changes: 84 additions & 0 deletions tests/test_radio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from dash import Dash, html, Output, Input, _dash_renderer
import dash_mantine_components as dmc

_dash_renderer._set_react_version("18.2.0")


def radiogroup_app(**kwargs):
app = Dash(__name__)

app.layout = dmc.MantineProvider(
html.Div([
dmc.RadioGroup(
id="radio-group",
children=dmc.Group([
dmc.Radio(value="option1", label="Option 1"),
dmc.Radio(value="option2", label="Option 2"),
dmc.Radio(value="option3", label="Option 3"),
]),
**kwargs,
),
html.Div(id="output")
])
)

@app.callback(
Output("output", "children"),
Input("radio-group", "value")
)
def update_output(selected_values):
return f'Selected: {selected_values}'

return app


def test_001ra_radio_group(dash_duo):

app = radiogroup_app()
dash_duo.start_server(app)

# Wait for the app to load
dash_duo.wait_for_element("div[aria-labelledby='radio-group-label']")

option1 = dash_duo.find_element("input[value='option1']")
option2 = dash_duo.find_element("input[value='option2']")
option1.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: option1")
option2.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: option2")
# Not deselectable
option2.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: option2")

assert dash_duo.get_logs() == []


def test_002ra_radio_group_deselectable(dash_duo):

app = radiogroup_app(deselectable=True)
dash_duo.start_server(app)

# Wait for the app to load
dash_duo.wait_for_element("div[aria-labelledby='radio-group-label']")

option1 = dash_duo.find_element("input[value='option1']")
option1.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: option1")
option1.click()
dash_duo.wait_for_text_to_equal("#output", "Selected: None")

assert dash_duo.get_logs() == []


def test_003ra_single_radio(dash_duo):

app = Dash(__name__)

app.layout = dmc.MantineProvider(dmc.Radio(label="Radio a", value="a", id="a"))

dash_duo.start_server(app)

# Wait for the app to load
dash_duo.wait_for_element("#a")

assert dash_duo.get_logs() == []

0 comments on commit a17de74

Please sign in to comment.