fix inability to remove SwitcherBar icons

This commit is contained in:
Aran-Fey
2024-06-30 19:19:02 +02:00
parent 4fe2506fed
commit 5031435b2c
19 changed files with 84 additions and 170 deletions
+3 -6
View File
@@ -2,7 +2,6 @@ import { applySwitcheroo } from '../designApplication';
import { ColorSet, ComponentId } from '../dataModels';
import { ComponentBase, ComponentState } from './componentBase';
import { RippleEffect } from '../rippleEffect';
import { firstDefined } from '../utils';
import { markEventAsHandled } from '../eventHandling';
type AbstractButtonState = ComponentState & {
@@ -109,13 +108,11 @@ abstract class AbstractButtonComponent extends ComponentBase {
) {
// It looks ugly if every new button is initially greyed out, so for
// the styling ignore `self.isStillInitiallyDisabled`.
let is_sensitive: boolean = firstDefined(
deltaState.is_sensitive,
this.state['is_sensitive']
);
let is_sensitive =
deltaState.is_sensitive ?? this.state.is_sensitive;
let colorSet = is_sensitive
? firstDefined(deltaState.color, this.state['color'])
? deltaState.color ?? this.state.color
: 'disabled';
// If no new colorset is specified, bump to the next palette. This
+4 -23
View File
@@ -69,29 +69,10 @@ export class CalendarComponent extends ComponentBase {
] = Array.from(headerElement.children) as HTMLElement[];
// Initialize icons
applyIcon(
this.prevYearButton,
'material/keyboard-double-arrow-left',
'currentColor'
);
applyIcon(
this.prevMonthButton,
'material/keyboard-arrow-left',
'currentColor'
);
applyIcon(
this.nextMonthButton,
'material/keyboard-arrow-right',
'currentColor'
);
applyIcon(
this.nextYearButton,
'material/keyboard-double-arrow-right',
'currentColor'
);
applyIcon(this.prevYearButton, 'material/keyboard_double_arrow_left');
applyIcon(this.prevMonthButton, 'material/keyboard_arrow_left');
applyIcon(this.nextMonthButton, 'material/keyboard_arrow_right');
applyIcon(this.nextYearButton, 'material/keyboard_double_arrow_right');
// Initialize the state
this.displayedYear = this.state.selectedYear;
+7 -29
View File
@@ -7,7 +7,7 @@ import { ComponentBase, ComponentState } from './componentBase';
import hljs from 'highlight.js/lib/common';
import { Language } from 'highlight.js';
import { setClipboard, firstDefined } from '../utils';
import { setClipboard } from '../utils';
import { applyIcon } from '../designApplication';
import { markEventAsHandled } from '../eventHandling';
@@ -104,11 +104,7 @@ export function convertDivToCodeBlock(
labelElement.textContent = languageNiceName;
// Initialize the copy button
applyIcon(
copyButton,
'material/content-copy',
'var(--rio-local-text-color)'
);
applyIcon(copyButton, 'material/content_copy');
copyButton.addEventListener('click', (event) => {
const codeToCopy = (preElement as HTMLPreElement).textContent ?? '';
@@ -116,19 +112,11 @@ export function convertDivToCodeBlock(
setClipboard(codeToCopy);
copyButton.title = 'Copied!';
applyIcon(
copyButton,
'material/done',
'var(--rio-local-text-color)'
);
applyIcon(copyButton, 'material/done');
setTimeout(() => {
copyButton.title = 'Copy code';
applyIcon(
copyButton,
'material/content-copy',
'var(--rio-local-text-color)'
);
applyIcon(copyButton, 'material/content_copy');
}, 5000);
markEventAsHandled(event);
@@ -152,22 +140,12 @@ export class CodeBlockComponent extends ComponentBase {
): void {
super.updateElement(deltaState, latentComponents);
// Find the value sto use
let code = firstDefined(deltaState.code, this.state.code);
let language = firstDefined(deltaState.language, this.state.language);
let displayControls = firstDefined(
deltaState.show_controls,
this.state.show_controls
);
// Re-create the code block
convertDivToCodeBlock(
this.element as HTMLDivElement,
code,
language,
displayControls
deltaState.code ?? this.state.code,
deltaState.language ?? this.state.language,
deltaState.show_controls ?? this.state.show_controls
);
}
}
+2 -2
View File
@@ -109,14 +109,14 @@ export class CodeExplorerComponent extends ComponentBase {
this.element.style.flexDirection = 'row';
applyIcon(
this.arrowElement,
'material/arrow-right-alt:fill',
'material/arrow_right_alt:fill',
'var(--rio-global-secondary-bg)'
);
} else {
this.element.style.flexDirection = 'column';
applyIcon(
this.arrowElement,
'material/arrow-downward:fill',
'material/arrow_downward:fill',
'var(--rio-global-secondary-bg)'
);
}
+1 -5
View File
@@ -11,11 +11,7 @@ export class ComponentPickerComponent extends ComponentBase {
let element = document.createElement('div');
element.classList.add('rio-component-picker');
applyIcon(
element,
'material/arrow-selector-tool:fill',
'var(--rio-local-text-color)'
);
applyIcon(element, 'material/arrow-selector-tool:fill');
element.addEventListener('click', this.pickComponent.bind(this));
+2 -6
View File
@@ -167,11 +167,7 @@ export class ComponentTreeComponent extends ComponentBase {
header.insertBefore(iconElement, header.firstChild);
if (children.length > 0) {
applyIcon(
iconElement,
'material/keyboard-arrow-right',
'currentColor'
);
applyIcon(iconElement, 'material/keyboard-arrow-right');
}
node.appendChild(header);
@@ -227,7 +223,7 @@ export class ComponentTreeComponent extends ComponentBase {
header.appendChild(iconElement);
iconElement.title = tooltip;
applyIcon(iconElement, icon, 'currentColor');
applyIcon(iconElement, icon);
}
// Click...
+2 -10
View File
@@ -34,11 +34,7 @@ export class DropdownComponent extends ComponentBase {
// Add an arrow icon
let arrowElement = document.createElement('div');
arrowElement.classList.add('rio-dropdown-arrow');
applyIcon(
arrowElement,
'material/expand-more',
'var(--rio-local-text-color)'
);
applyIcon(arrowElement, 'material/expand_more');
this.inputBox.suffixElement = arrowElement;
// Create the popup
@@ -408,11 +404,7 @@ export class DropdownComponent extends ComponentBase {
// Was anything found?
if (this.optionsElement.children.length === 0) {
applyIcon(
this.optionsElement,
'material/error',
'var(--rio-local-text-color)'
);
applyIcon(this.optionsElement, 'material/error');
// The icon is loaded asynchronously, so make sure to give the
// element some space
+1 -5
View File
@@ -73,11 +73,7 @@ export class ImageComponent extends ComponentBase {
private _onError(event: string | Event): void {
this.imageElement.classList.remove('rio-content-loading');
applyIcon(
this.element,
'material/broken-image',
'var(--rio-local-text-color)'
);
applyIcon(this.element, 'material/broken-image');
this.sendMessageToBackend({
type: 'onError',
+5 -13
View File
@@ -1,6 +1,5 @@
import { ComponentBase, ComponentState } from './componentBase';
import { ComponentId } from '../dataModels';
import { firstDefined } from '../utils';
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
const HARDWARE_KEY_MAP = {
@@ -710,18 +709,11 @@ export class KeyEventListenerComponent extends ComponentBase {
let element = this.element;
let reportKeyDown = firstDefined(
deltaState.reportKeyDown,
this.state.reportKeyDown
);
let reportKeyUp = firstDefined(
deltaState.reportKeyUp,
this.state.reportKeyUp
);
let reportKeyPress = firstDefined(
deltaState.reportKeyPress,
this.state.reportKeyPress
);
let reportKeyDown =
deltaState.reportKeyDown ?? this.state.reportKeyDown;
let reportKeyUp = deltaState.reportKeyUp ?? this.state.reportKeyUp;
let reportKeyPress =
deltaState.reportKeyPress ?? this.state.reportKeyPress;
if (reportKeyDown || reportKeyPress) {
element.onkeydown = (e: KeyboardEvent) => {
-6
View File
@@ -112,12 +112,6 @@ function hijackLocalLinks(div: HTMLElement): void {
export class MarkdownComponent extends ComponentBase {
state: Required<MarkdownState>;
// Since laying out markdown is time intensive, this component does its best
// not to re-layout unless needed. This is done by setting the height
// request lazily, and only if the width has changed. This value here is the
// component's allocated width when the height request was last set.
private heightRequestAssumesWidth: number;
createElement(): HTMLElement {
const element = document.createElement('div');
element.classList.add('rio-markdown');
+1 -1
View File
@@ -63,7 +63,7 @@ export class RevealerComponent extends ComponentBase {
) as HTMLElement;
// Initialize them
applyIcon(this.arrowElement, 'material/expand_more', 'currentColor');
applyIcon(this.arrowElement, 'material/expand_more');
this.rippleInstance = new RippleEffect(element, {
triggerOnPress: false,
+4 -5
View File
@@ -1,6 +1,5 @@
import { applySwitcheroo } from '../designApplication';
import { markEventAsHandled } from '../eventHandling';
import { firstDefined } from '../utils';
import { ComponentBase, ComponentState } from './componentBase';
export type SliderState = ComponentState & {
@@ -152,10 +151,10 @@ export class SliderComponent extends ComponentBase {
) {
// The server can send invalid values due to reconciliation. Fix
// them.
let value = firstDefined(deltaState.value, this.state.value);
let step = firstDefined(deltaState.step, this.state.step);
let minimum = firstDefined(deltaState.minimum, this.state.minimum);
let maximum = firstDefined(deltaState.maximum, this.state.maximum);
let value = deltaState.value ?? this.state.value;
let step = deltaState.step ?? this.state.step;
let minimum = deltaState.minimum ?? this.state.minimum;
let maximum = deltaState.maximum ?? this.state.maximum;
// Bring the value into a valid range
value = Math.max(minimum, Math.min(maximum, value));
+11 -16
View File
@@ -1,16 +1,16 @@
import { ComponentBase, ComponentState } from './componentBase';
import { ColorSet } from '../dataModels';
import { applySwitcheroo } from '../designApplication';
import { firstDefined } from '../utils';
import { applyIcon, applySwitcheroo } from '../designApplication';
import { MappingTween } from '../tweens/mappingTweens';
import { BaseTween } from '../tweens/baseTween';
import { KineticTween } from '../tweens/kineticTween';
import { pixelsPerRem } from '../app';
import { firstDefined } from '../utils';
export type SwitcherBarState = ComponentState & {
_type_: 'SwitcherBar-builtin';
names?: string[];
icon_svg_sources?: (string | null)[];
icons?: (string | null)[] | null;
color?: ColorSet;
orientation?: 'horizontal' | 'vertical';
spacing?: number;
@@ -83,7 +83,7 @@ export class SwitcherBarComponent extends ComponentBase {
this.resizeObserver.disconnect();
}
onResize(entries: ResizeObserverEntry[]): void {
onResize(): void {
// Update the marker position
if (this.state.selectedName !== null) {
this.markerAtAnimationEnd = this.getMarkerTarget()!;
@@ -253,24 +253,22 @@ export class SwitcherBarComponent extends ComponentBase {
result.classList.add('rio-switcher-bar-options');
result.style.gap = `${this.state.spacing}rem`;
let names = firstDefined(deltaState.names, this.state.names);
let iconSvgSources = firstDefined(
deltaState.icon_svg_sources,
this.state.icon_svg_sources
);
let names = deltaState.names ?? this.state.names;
let icons = firstDefined(deltaState.icons, this.state.icons);
// Iterate over both
for (let i = 0; i < names.length; i++) {
let name = names[i];
let iconSvg = iconSvgSources[i];
let optionElement = document.createElement('div');
optionElement.classList.add('rio-switcher-bar-option');
result.appendChild(optionElement);
// Icon
if (iconSvg !== null) {
optionElement.innerHTML = iconSvg;
if (icons !== null && icons[i] !== null) {
let iconContainer = document.createElement('div');
optionElement.appendChild(iconContainer);
applyIcon(iconContainer, icons[i]!);
}
// Text
@@ -294,10 +292,7 @@ export class SwitcherBarComponent extends ComponentBase {
super.updateElement(deltaState, latentComponents);
// Have the options changed?
if (
deltaState.names !== undefined ||
deltaState.icon_svg_sources !== undefined
) {
if (deltaState.names !== undefined || deltaState.icons !== undefined) {
this.innerElement.innerHTML = '';
this.markerElement.innerHTML = '';
+1 -1
View File
@@ -270,7 +270,7 @@ export async function applyIcon(
| Color
| ColorSet
| 'dim'
| string // A CSS color value
| string = 'currentColor' // A CSS color value
): Promise<void> {
// Avoid races: When calling this function multiple times on the same
// element it can sometimes assign the first icon AFTER the second one, thus
+4 -4
View File
@@ -131,16 +131,16 @@ export function zip<T1, T2>(
return result;
}
/// Returns the first argument that isn't `undefined`. Returns `undefined` if
/// none of the arguments are defined.
export function firstDefined(...args: any[]): any {
/// Returns the first argument that isn't `undefined`. Throws an error if all
/// arguments are `undefined`.
export function firstDefined<T>(...args: (T | undefined)[]): T {
for (let arg of args) {
if (arg !== undefined) {
return arg;
}
}
return undefined;
throw new Error('All arguments were `undefined`');
}
/// Removes `oldElement` from the DOM and inserts `newElement` at its position
+1 -1
View File
@@ -2347,7 +2347,7 @@ $rio-input-box-text-distance-from-bottom: 0.4rem; // To be aligned with the <inp
color 0.1s ease-out;
}
.rio-switcher-bar-option > svg {
.rio-switcher-bar-option svg {
width: 1.8rem;
height: 1.8rem;
margin-bottom: 0.5rem;
+26 -31
View File
@@ -120,9 +120,9 @@ class SwitcherBar(FundamentalComponent, Generic[T]):
`experimental`: True
"""
names: list[str]
values: list[T]
icon_svg_sources: list[str | None]
names: Sequence[str]
values: Sequence[T]
icons: Sequence[str | None] | None
color: rio.ColorSet
orientation: Literal["horizontal", "vertical"]
spacing: float
@@ -132,9 +132,9 @@ class SwitcherBar(FundamentalComponent, Generic[T]):
def __init__(
self,
values: list[T],
values: Sequence[T],
*,
names: list[str] | None = None,
names: Sequence[str] | None = None,
icons: Sequence[str | None] | None = None,
color: rio.ColorSet = "keep",
orientation: Literal["horizontal", "vertical"] = "horizontal",
@@ -179,44 +179,44 @@ class SwitcherBar(FundamentalComponent, Generic[T]):
# SCROLLING-REWORK scroll_y=scroll_y,
)
# Names default to the string representation of the values
if names is None:
names = [str(value) for value in values]
self.names = names
self.values = values
self.icons = icons
self.selected_value = selected_value
self.color = color
self.orientation = orientation
self.spacing = spacing
self.allow_none = allow_none
self.on_change = on_change
def __post_init__(self) -> None:
values = self.values
if not values:
raise ValueError("`SwitcherBar` must have at least one option.")
# Names default to the string representation of the values
if names is None:
self.names = [str(value) for value in values]
else:
if len(names) != len(values):
raise ValueError("`names` must be the same length as `values`.")
if len(self.names) != len(values):
raise ValueError("`names` must be the same length as `values`.")
self.names = names
# Icons
icons = self.icons
# Icons default to `None`. Also, fetch their SVG sources so any errors
# are raised now, rather than later.
if icons is None:
self.icon_svg_sources = [None] * len(values)
else:
if icons is not None:
if len(icons) != len(values):
raise ValueError("`icons` must be the same length as `values`.")
self.icon_svg_sources = [
None if icon is None else icon_registry.get_icon_svg(icon)
for icon in icons
]
# Make sure the icon names are valid
for icon in icons:
if icon is not None:
icon_registry.get_icon_svg(icon)
self.selected_value = selected_value
def __post_init__(self) -> None:
# Make sure a value is selected, if needed
if self.selected_value is None and not self.allow_none:
self.selected_value = self.values[0]
self.selected_value = values[0]
def _fetch_selected_name(self) -> str | None:
# None is fine
@@ -238,15 +238,10 @@ class SwitcherBar(FundamentalComponent, Generic[T]):
return self.names[0]
def _custom_serialize(self) -> JsonDoc:
result = {
"optionNames": self.names,
"optionIcons": self.icon_svg_sources,
return {
"selectedName": self._fetch_selected_name(),
"color": self.session.theme._serialize_colorset(self.color),
}
return result
async def _on_message(self, msg: Any) -> None:
# Parse the message
assert isinstance(msg, dict), msg
+1 -1
View File
@@ -84,7 +84,7 @@ def get_child_component_containing_attribute_names(
attr_names.append(attr_name)
# : list[Component]
elif (
serializer.func is serialization._serialize_list
serializer.func is serialization._serialize_sequence
and serializer.keywords["item_serializer"]
is serialization._serialize_child_component
):
+8 -5
View File
@@ -1,10 +1,12 @@
from __future__ import annotations
import collections.abc
import enum
import functools
import inspect
import json
import types
import typing
from typing import * # type: ignore
import introspection.types
@@ -195,10 +197,10 @@ def _serialize_child_component(
return component._id
def _serialize_list(
sess: session.Session, list_: list[T], item_serializer: Serializer[T]
def _serialize_sequence(
sess: session.Session, sequence: Sequence[T], item_serializer: Serializer[T]
) -> Jsonable:
return [item_serializer(sess, item) for item in list_]
return [item_serializer(sess, item) for item in sequence]
def _serialize_enum(
@@ -262,12 +264,13 @@ def _get_serializer_for_annotation(
return functools.partial(_serialize_enum, as_type=annotation)
# Sequences of serializable values
if origin is list:
if origin in (list, typing.Sequence, collections.abc.Sequence):
item_serializer = _get_serializer_for_annotation(args[0])
if item_serializer is None:
return None
return functools.partial(
_serialize_list, item_serializer=item_serializer
_serialize_sequence, item_serializer=item_serializer
)
# Literal