Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add CheckboxCard, CheckboxIndicator, RadioCard, RadioIndicator #486 #509

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

deadkex
Copy link

@deadkex deadkex commented Feb 9, 2025

Played around with implementing CheckboxCards.
Some help would be appreciated.
See comments for examples.

@AnnMarieW
Copy link
Collaborator

AnnMarieW commented Feb 9, 2025

Hi @deadkex

This looks awesome! Thanks for the great PR 🚀

Callbacks work perfectly after fixing the typo in the id

Try changing

dmc.Box(id="output2", children="Output 1:"),

To

dmc.Box(id="output1", children="Output 1:"),

I'll take a closer look at the rest later, but just wanted you to know that the callbacks work 🙂

@deadkex
Copy link
Author

deadkex commented Feb 9, 2025

grafik

import dash
from dash import Dash, Input, Output, callback

import dash_mantine_components as dmc

dash._dash_renderer._set_react_version('18.2.0')  # noqa
app = Dash(external_stylesheets=dmc.styles.ALL)

app.layout = dmc.MantineProvider(
    forceColorScheme="dark",
    children=dmc.Box([
        dmc.CheckboxGroup(
            id="cbg",
            label="CheckboxCard Group",
            children=dmc.Group([
                dmc.CheckboxCard(
                    className="checkboxcardroot",
                    children=[dmc.Stack([dmc.Text("CheckboxCard 1, no Checkbox", size="xl"),
                                         dmc.Text("description", c="dimmed")])],
                    value="1"),
                dmc.CheckboxCard(
                    className="checkboxcardroot",
                    children=dmc.Group(wrap="nowrap", align="flex-start", children=[
                        dmc.CheckboxIndicator(),
                        dmc.Stack([dmc.Text("CheckboxCard 2", size="xl"), dmc.Badge("A badge")])]),
                    value="2"),
                dmc.CheckboxCard(
                    className="customcheckboxcardroot",
                    children=[dmc.Stack([dmc.Text("Custom CheckboxCard 3", size="xl"),
                                         dmc.Text("description", c="dimmed")])],
                    value="3"),
            ],
                mt=10,
            ),
            value=["1"],
        ),
        dmc.Box(id="output1", children="Output 1:"),
        dmc.Space(h=30),

        dmc.Text("Card without styles:"),
        dmc.CheckboxCard(
            id="cb",
            withBorder=True,
            children=dmc.Group(wrap="nowrap", align="flex-start", children=[
                dmc.CheckboxIndicator(),
                dmc.Stack([dmc.Text("CheckboxCard", size="xl"), dmc.Text("description", c="dimmed")])])
        ),
        dmc.Box(id="output2", children="Output 2:"),
    ],
        pt="10px", px="20%"
    )
)


@callback(
    Output("output1", "children"),
    Input("cbg", "value")
)
def on_cbg(event):
    if event:
        return f"Output 1: {event}"
    return "nothing"


@callback(
    Output("output2", "children"),
    Input("cb", "checked")
)
def on_cb(event):
    if event:
        return f"Output 2: {event}"
    return "nothing"


if __name__ == "__main__":
    app.run(debug=True)
.checkboxcardroot {
    position: relative;
    padding: var(--mantine-spacing-md);
    transition: border-color 150ms ease;

    &[data-checked] {
        border-color: var(--mantine-primary-color-filled);
    }
}

.checkboxcardroot:hover {
    background-color: light-dark(
            var(--mantine-color-gray-0),
            var(--mantine-color-dark-6)
    );
}

.customcheckboxcardroot {
    position: relative;
    padding: var(--mantine-spacing-md);
    transition: border-color 150ms ease;

    &[data-checked] {
        border-color: var(--mantine-primary-color-filled);
    }
}

.customcheckboxcardroot::after {
    position: absolute;
    inset-block-start: 2px;
    inset-inline-end: 2px;
    width: 0;
    height: 0;
    opacity: 0;
    transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
    border-block-end: 10px solid transparent;
    border-inline-start: 10px solid transparent;
    border-start-end-radius: 6px;
    content: '';
}

.customcheckboxcardroot[data-checked="true"]::after {
        opacity: 1;
        border: 10px solid #1677ff;
        border-block-end: 10px solid transparent;
        border-inline-start: 10px solid transparent;
        border-start-end-radius: 6px;
}

@deadkex deadkex changed the title add CheckboxCard and CheckboxIndicator #486 add CheckboxCard, CheckboxIndicator, RadioCard, RadioIndicator #486 Feb 9, 2025
@deadkex
Copy link
Author

deadkex commented Feb 9, 2025

grafik

import dash
from dash import Dash, Input, Output, callback

import dash_mantine_components as dmc

dash._dash_renderer._set_react_version('18.2.0')  # noqa
app = Dash(external_stylesheets=dmc.styles.ALL)

app.layout = dmc.MantineProvider(
    forceColorScheme="dark",
    children=dmc.Box([
        dmc.RadioGroup(
            id="rg",
            label="RadioCard Group",
            children=dmc.Group([
                dmc.RadioCard(
                    className="checkboxcardroot",
                    children=dmc.Group(wrap="nowrap", align="flex-start", children=[
                        dmc.RadioIndicator(),
                        dmc.Stack([dmc.Text("RadioCard 1", size="xl"), dmc.Badge("A badge")])]),
                    value="1"),
                dmc.RadioCard(
                    className="customcheckboxcardroot",
                    children=[dmc.Stack([dmc.Text("Custom RadioCard 2", size="xl"),
                                         dmc.Text("description", c="dimmed")])],
                    value="2"),
                dmc.RadioCard(
                    className="checkboxcardroot",
                    children=dmc.Group(wrap="nowrap", align="flex-start", children=[
                        dmc.RadioIndicator(),
                        dmc.Text("RadioCard 3", size="xl")]),
                    value="3"),
            ],
                mt=10,
            ),
            value="1",
        ),
        dmc.Box(id="output1", children="Output 1:"),
        dmc.Space(h=30),
    ],
        pt="10px", px="35%"
    )
)


@callback(
    Output("output1", "children"),
    Input("rg", "value")
)
def on_rg(event):
    if event:
        return f"Output 1: {event}"
    return "nothing"


if __name__ == "__main__":
    app.run(debug=True)

@AnnMarieW
Copy link
Collaborator

AnnMarieW commented Feb 10, 2025

@deadkex

This looks great and thanks for adding the RadioCard too 🏆

To DO:

  • tests
  • changelog entry
  • alexj review
  • dmc 1.0 released

Are you familiar with Dash testing? If so, we need to add a couple simple tests for the dash props in RadioCard and CheckboxCard to test that callbacks work both individually and with the RadioGroup and CheckboxGroup. It's not necessary to add separate tests for RadioIndicator and CheckboxIndicator since they just have pass through props.

Let me know if you run into any issues and I can help with the tests.

Before merging, ideally, I'd like to have dash 3.0 and dmc 1,0 released. If this causes a long delay, let's revisit.

@deadkex
Copy link
Author

deadkex commented Feb 10, 2025

  • tests
  • changelog entry

I didn't run the tests locally, maybe they need more adjustments but i think they should work.

@AnnMarieW
Copy link
Collaborator

Hi @deadkex
Thanks for adding the test. I have a few fixes that will make all but one test pass. Would you like me to push the changes to your PR? (Or i could just post them as comments here.)

It looks like the deselectable prop with the radio card doesn't work. Do you think it's because of this line (using input instead of button?) https://github.com/snehilvj/dash-mantine-components/blob/master/src/ts/components/core/radio/RadioGroup.tsx#L43

Did you get an example working locally with the deselectable=True?

@deadkex
Copy link
Author

deadkex commented Feb 11, 2025

Would you like me to push the changes to your PR?

Yes just push them.

It looks like the deselectable prop with the radio card doesn't work. Do you think it's because of this line (using input instead of button?) https://github.com/snehilvj/dash-mantine-components/blob/master/src/ts/components/core/radio/RadioGroup.tsx#L43

Seems like it has to do with that but i'm unsure how/what to change to make it work.
I had to add RadioCardGroupContext which uses HTMLButtonElement instead of HTMLInputElement when adding RadioCards.

Did you get an example working locally with the deselectable=True?

Seems like i missed that, nope does not work.

@AnnMarieW
Copy link
Collaborator

Ok, just the deselectable radio test fails now. I'll leave that in for now in case we come up with a solution, otherwise, we can document this as a limitation.

@RenaudLN
Copy link
Contributor

RenaudLN commented Feb 11, 2025

Hey @deadkex, at @AnnMarieW's request I had a look at making the deselectable option work. There are a few changes required. The reason why is that the 'RadioCard' is not an input and therefore has no value attribute on the html side. The changes impact both radiocard and radio but it all works on my end.

radioGroupContext

interface RadioGroupContextProps {
    radioOnClick?: (val?: string) => void;
}

radioGroup

    const handleRadioClick = (val?: string) => {
        if (val === value) {
            setProps({ value: null });
        }
    };

radio

const Radio = (props: Props) => {
    const {
        setProps,
        loading_state,
        persistence,
        persisted_props,
        persistence_type,
        value,
        ...others
    } = props;

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

    return (
        <MantineRadio
            data-dash-is-loading={getLoadingState(loading_state) || undefined}
            onChange={(ev) => setProps({ checked: ev.currentTarget.checked })}
            onClick={radioOnClick ? () => radioOnClick(value) : null}
            value={value}
            {...others}
        />
    );
};

radioCard

const RadioCard = (props: Props) => {
    const {
        children,
        setProps,
        loading_state,
        persistence,
        persisted_props,
        persistence_type,
        value,
        ...others
    } = props;

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

    return (
        <MantineRadioCard
            data-dash-is-loading={getLoadingState(loading_state) || undefined}
            onClick={radioOnClick ? () => radioOnClick(value) : null}
            value={value}
            {...others}
        >
            {children}
        </MantineRadioCard>
    );
};

And remove radioCardGroupContext.tsx and update the context import in radioCard.

With this all the radio tests work for me!

@deadkex
Copy link
Author

deadkex commented Feb 12, 2025

@RenaudLN Thank you for the help, i added your proposed changes

@deadkex deadkex marked this pull request as ready for review February 12, 2025 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants