master
Sven Slootweg 4 years ago
parent 63fee8ca26
commit 7dff6bfdef

@ -5,3 +5,16 @@
# Name ideas
- SnapUI (snap-ui)
# Feature ideas
SetIconSource wrapper, like SetTheme, for providing a custom Icon component (eg. when using an icon font instead of images), and some way to have icon fallbacks
# Context design rules
- Global contexts named normally; control-specific contexts should be $control_$name, like with custom themeing rules
- Event handler contexts: $control_on$Event
- A control with interceptable event handler should always require a unique ID (either implicit or explicit) when interception is used; this is passed as the first argument to the event handler
- Any same-named event handlers defined on the control directly should be called *even if it is intercepted*, since the user explicitly expressed that they want custom behaviour anyway
- 'Selected item' contexts for eg. buttonsets: $control_selectedID
- A selectable item should *always* wrap arbitrary content in a null selectedID context, to avoid passing this context beyond the first matching control

@ -0,0 +1,9 @@
"use strict";
const React = require("react");
// FIXME: Port over the remaining stand-alone contexts to the new context design
module.exports = {
button_onClick: React.createContext(),
button_selectedID: React.createContext()
};

@ -0,0 +1,36 @@
"use strict";
const React = require("react");
const asExpression = require("as-expression");
const useTheme = require("../../util/themeable");
const defaultStyle = require("./style.css");
const contexts = require("../../contexts");
module.exports = function ButtonSet({ direction, choice, children }) {
let { withTheme } = useTheme({ control: "buttonSet", defaultStyle });
let [ selectedItem, setSelectedItem ] = React.useState();
// direction: horizontal, vertical
// FIXME: Validate direction
let wrappedChildren = asExpression(() => {
if (choice === true) {
return (
<contexts.button_onClick.Provider value={setSelectedItem}>
<contexts.button_selectedID.Provider value={selectedItem}>
{children}
</contexts.button_selectedID.Provider>
</contexts.button_onClick.Provider>
);
} else {
return children;
}
});
return (
<div className={withTheme("buttonSet", `direction-${direction}`)}>
{wrappedChildren}
</div>
);
};

@ -0,0 +1,11 @@
.buttonSet {
composes: combinedButton from "../shared.css";
}
.direction-horizontal {
grid-auto-columns: 1fr;
}
.direction-vertical {
grid-auto-rows: 1fr;
}

@ -4,22 +4,45 @@ const React = require("react");
const defaultStyle = require("./style.css");
const useTheme = require("../../util/themeable");
const useID = require("../../util/use-id");
const contexts = require("../../contexts");
const composeHandlers = require("../../util/compose-handlers");
const Icon = require("../icon");
module.exports = function Button({ type, onClick, label, icon }) {
module.exports = function Button({ type, onClick, children, icon, default: default_ }) {
// TODO: Validate type?
// TODO: Content override instead of label/icon?
// TODO: How does icon + custom HTML children end up looking? Should we use a grid to ensure that the icon is always displayed on the left in its own column?
let { withTheme } = useTheme({ control: "button", defaultStyle });
let id = useID();
let selectedID = React.useContext(contexts.button_selectedID);
let capturedOnClick = React.useContext(contexts.button_onClick);
React.useEffect(() => {
if (capturedOnClick != null && default_ === true) {
// This is a default selection in a choice list of some sort, eg. a button-set - so we simulate a click event.
capturedOnClick(id);
}
}, [ capturedOnClick, id ]);
let isSelected = (id === selectedID);
let onClickAll = composeHandlers([
capturedOnClick,
onClick
]);
return (
<button className={withTheme("button", `style-${type}`)} onClick={onClick}>
<span className="buttonContents">
<button className={withTheme("button", `type-${type}`, { selected: isSelected })} onClick={() => onClickAll(id)}>
<span className={withTheme("buttonContents")}>
{(icon != null)
? <Icon icon={icon} />
: null
}
{label}
<contexts.button_selectedID.Provider value={null}>
{children}
</contexts.button_selectedID.Provider>
</span>
</button>
);

@ -0,0 +1,44 @@
:import("../icon/style.css") {
icon: icon;
}
.button {
composes: centerContent from "../shared.css";
box-sizing: border-box;
padding: 3px;
font-size: 12px;
border-radius: 2px;
}
.buttonContents {
padding: 0px 6px;
}
.type-inline, .type-inlineTiny {
text-align: left;
.icon {
display: inline;
vertical-align: middle;
position: relative;
top: -1px;
}
}
.type-inline {
.icon {
width: 16px;
height: 16px;
margin: 0px 9px 0px 2px;
}
}
.type-inlineTiny {
.icon {
width: 12px;
height: 12px;
margin: 0px 11px 0px 4px;
}
}
/* FIXME: buttonSetActive */

@ -66,7 +66,7 @@ module.exports = function ListItem({ label, id, children }) {
return (
<React.Fragment>
{/* FIXME: item_selected capitalization */}
<div className={withTheme(["item", { item_selected: isSelected }])} style={style} onMouseDown={handleMouseDown} onClick={handleClick}>
<div className={withTheme("item", { item_selected: isSelected })} style={style} onMouseDown={handleMouseDown} onClick={handleClick}>
{(children != null)
? <Expander expanded={expanded} onClick={toggleExpanded} />
: <ExpanderPlaceholder />}

@ -3,11 +3,10 @@
const React = require("react");
const useMeasure = require("react-use-measure");
const matchValue = require("match-value");
const insecureNanoid = require("nanoid/non-secure").nanoid;
const defaultStyle = require("./style.css");
const useTheme = require("../../util/themeable");
const useGuaranteedMemo = require("../../util/use-guaranteed-memo");
const useID = require("../../util/use-id");
const isEventInsideRef = require("../../util/is-event-inside-ref");
const comparePath = require("../../util/compare-path");
const MenuItemContext = require("../../contexts/menu-item");
@ -20,7 +19,7 @@ module.exports = function MenuItem({ label, title, menu, onClick }) {
let submenuRef = React.useRef();
let itemContext = React.useContext(MenuItemContext);
let path = React.useContext(MenuPathContext);
let id = useGuaranteedMemo(() => insecureNanoid()); // insecure is fine here, these IDs are not interesting to an attacker
let id = useID(); // insecure is fine here, these IDs are not interesting to an attacker
let ownPath = path.concat([ id ]); // FIXME: memoize?
let isSelected = itemContext.isActive && comparePath(itemContext.selectedPath, ownPath);
@ -65,11 +64,11 @@ module.exports = function MenuItem({ label, title, menu, onClick }) {
},
};
let buttonClasses = withTheme([
let buttonClasses = withTheme(
"item",
{ item_selected: isSelected },
{ item_directPress: (menu == null) }
]);
);
return (
<MenuPathContext.Provider value={ownPath}>

@ -1,4 +1,18 @@
:import("../button/style.css") { button: button; }
:import("../shared.css") { combinedButton: combinedButton; }
.ribbon {
composes: notSelectable from "../shared.css";
display: flex;
.button {
width: 100%;
height: 100%;
min-width: 16px;
}
.combinedButton {
display: grid; /* block, not inline */
height: 100%;
}
}

@ -15,3 +15,14 @@
justify-content: center;
text-align: center;
}
.combinedButton {
display: inline-grid;
box-sizing: border-box;
border-radius: 2px;
overflow: hidden;
& > * {
border-radius: 0px !important;
}
}

@ -3,6 +3,8 @@
module.exports = {
/* Multi-purpose controls */
Icon: require("./controls/icon/index.jsx"),
Button: require("./controls/button/index.jsx"),
ButtonSet: require("./controls/button-set/index.jsx"),
// Text: require("./controls/text.jsx"),
// /* Pane layout */

@ -2,6 +2,14 @@ $darkGray: rgb(34, 34, 34);
$hoverColor: rgba(113, 113, 113, 0.26);
$activeColor: $darkGray;
/* :export {
darkGray: $darkGray;
hoverColor: $hoverColor;
activeColor: $activeColor;
} */
/* FIXME: Casemap control names */
.edgeRaise {
box-shadow: inset -1px -1px rgba(30, 27, 27, 0.7),
inset 1px 1px rgba(135, 131, 131, 0.3);
@ -41,7 +49,7 @@ $activeColor: $darkGray;
}
}
.menu_menuBar {
.menu_menuBar, .ribbon_ribbon {
composes: bar;
}
@ -70,6 +78,21 @@ $activeColor: $darkGray;
}
}
.ribbon_ribbon {
composes: bar;
.button_button {
// background-color: #38383c;
background-color: magenta;
border: none;
&:hover {
background-color: $hoverColor;
}
&:active {
@include edge-lower;
background-color: $activeColor;
}
&.button_selected {
background-color: $darkGray;
}
}

@ -0,0 +1,11 @@
"use strict";
module.exports = function composeHandlers(handlers) {
let nonNullHandlers = handlers.filter((handler) => handler != null);
return function callOnEvent(... args) {
for (let handler of nonNullHandlers) {
handler(... args);
}
};
};

@ -23,10 +23,10 @@ module.exports = function useTheme({ control, defaultStyle }) {
? `${control}_`
: "";
function withTheme(classes) {
if (arguments.length > 1) {
console.warn(`WARNING: More than one argument specified to 'withTheme' call (second argument is ${JSON.stringify(arguments[1])})`);
}
function withTheme(... classes) {
// if (arguments.length > 1) {
// console.warn(`WARNING: More than one argument specified to 'withTheme' call (second argument is ${JSON.stringify(arguments[1])})`);
// }
let mappedClasses = extendClassnames(classes, (className) => {
return [

@ -0,0 +1,8 @@
"use strict";
const insecureNanoid = require("nanoid/non-secure").nanoid;
const useGuaranteedMemo = require("./use-guaranteed-memo");
module.exports = function useID() {
return useGuaranteedMemo(() => insecureNanoid());
};
Loading…
Cancel
Save