-
Notifications
You must be signed in to change notification settings - Fork 58
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
feat: Add dark-mode toggle switch #787
Conversation
R/input-switch.R
Outdated
mode <- rlang::arg_match(mode, c("light", "dark")) | ||
} | ||
|
||
if (any(!nzchar(rlang::names2(rlang::list2(...))))) { | ||
abort("All arguments in `...` must be named.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we already importFrom
for list2()
, and arg_match()
/names2()
is becoming common enough that I'd be inclined to also importFrom
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I never remember what's imported and what isn't. I'm open to importing just these functions, but if we're adding more imported functions from rlang, I'd rather just @import rlang
-- which also opens us up to using the rlang type checking functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I'm OK with that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll do that in a separate PR then! 😄
For posterity, it'd be nice to have a link to the shinycomponent source that you worked from in the PR comments Edit: Original source at https://github.com/wch/shinycomponent/blob/main/js/src/forge/dark-mode-switch.ts |
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM once some of the more minor nitpicks are addressed! 🌔
static shinyCustomMessageHandlers = { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
"bslib.toggle-dark-mode": ({ | ||
method, | ||
value, | ||
}: DarkModeMessageToggle): void => { | ||
// Similar to DarkModeSwitch.receiveMessage(), but we directly update the | ||
// Bootstrap attribute on the <html> element. Currently, all toggle switches | ||
// follow this value. | ||
|
||
if (method !== "toggle") return; | ||
|
||
if (typeof value === "undefined" || value === null) { | ||
const current = document.documentElement.dataset.bsTheme || "light"; | ||
value = current === "light" ? "dark" : "light"; | ||
} | ||
|
||
document.documentElement.dataset.bsTheme = value; | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this actually should be separate, and should go inside of bslibShiny.ts
.
With the way that the code is currently structured, the dark mode state is part of the page. The switch component reflects the state and lets you change the state, but the <html>
tag is the source of truth for the dark mode state.
If someone is programmatically setting the dark mode state from R, it's plausible that they'd want to do it even if there's no dark mode switch on the page. However, if the app doesn't contain the dark mode switch (or any other components in webComponents.js
) then this code won't be loaded, and they won't be able to programmatically set light/dark mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line of reasoning also implies that the dark mode/theme state should be available via clientData
, but that could be done at some point in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone is programmatically setting the dark mode state from R, it's plausible that they'd want to do it even if there's no dark mode switch on the page. However, if the app doesn't contain the dark mode switch (or any other components in webComponents.js) then this code won't be loaded, and they won't be able to programmatically set light/dark mode.
Unfortunately, moving this code to bslibShiny.ts
doesn't solve this problem, since all components deps are individually added when each component is added to the page.
I think it makes sense to require the input_dark_mode()
to be added to the app as a way for the app author to explicitly support dark mode, both to give the app user the opportunity to pick the color mode, as well as a way for us to explicitly check the Bootstrap version (which could be 3 or 4 and therefore not supporting dark mode).
Edit: clarified that component deps are individually added, even the bslibShiny dep
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a side note, we're probably going into a direction where if any bslib UI is used, then all component-specific assets (bundled into a single htmlDependency()
) will come with it, which I think would "solve" this problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cpsievert true, and that would make the server side toggle work without the input, which I also think is okay. I'd rather keep as much of the implementation together in one place though, unless there a philosophically bigger reason why we would want to separate these pieces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see the reasons for keeping the code in this file... But, in the interest of having the component class encapsulate just the component's functionality, I think this message handler should be moved outside of the class.
}, | ||
}; | ||
|
||
private attribute = "data-shinytheme"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default value is set both here and inside of connectedCallback()
. There are two ways I can think of to simplify, both of which are defensible:
One is to use @property
here:
private attribute = "data-shinytheme"; | |
@property({ type: String}) | |
private attribute = "data-shinytheme"; |
And then you don't have to do manually set it in connectedCallback
.
The other option is to modify connectedCallback
so instead of this:
this.attribute = this.getAttribute("attribute") || "data-shinytheme";
it has this:
this.attribute = this.getAttribute("attribute") || this.attribute;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've add bslib-specific naming in many more places than I originally intended that at this point I think this could be a static value. Regardless, I opted for the second suggestion – a private value with an initial value that's possibly overwritten when the component is connected.
Adds the dark mode toggle switch web component to bslib from https://github.com/wch/shinycomponent/blob/f4d8080544a273d9e41287f678f4dfe0d39e4ffb/js/src/forge/dark-mode-switch.ts
cc @wch @nstrayer
TODO
input_dark_mode()
andtoggle_dark_mode()
Implementation notes
I made a few tweaks from the original implementation:
Moved all CSS custom properties that might be externally set up to the top of the
styles
array. I also tracked down their default values to ensure they work outside of shinycomponent and without open-props.styleThe component sets a configurable attribute on the document element, which can be controlled via the (static)
theme-attribute
attribute on the web component.The R constructor sets
--text-1
and--text-2
to--bs-emphasis-color
and--bs-tertiary-color
respectively.I also renamed
--vertical_correction
to--vertical-correction
(the_
causes trouble withhtmltools::css()
) and then unset it in the R constructor. I think we'll probably want to revisit those styles in the future once we see the component used in more contexts.I updated the naming of the
theme-value
attribute (instead ofthemevalue
) and I tweaked the buttontitle
text.A few other minor changes reflect that bslib has slightly different tooling for setting up input bindings from web components.
The R component wrapper allows app authors to set the color mode value. I also modified the component to reflect the current state into the webcomponent element and to respond to changes to that value, i.e. setting
theme-value="dark"
on the web component can be used to trigger a color mode change.I then tied this into a
receiveMessage()
method to be used by the Shiny input binding to toggle or set this attribute viatoggle_dark_mode_switch()
from R.Other new things added as suggested during the course of the PR including having all toggle watch the
document.documentElement
for changes in theme and reflect that back internally.Example
The key in this example is to add the dark mode switch to the nav inside
nav_item()
, pushed to the right with a precedingnav_spacer()
.Kapture.2023-09-06.at.10.21.37.mp4