From 7dff6bfdef0a2259a30cb834fb9f9326d9ea5218 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Fri, 28 Aug 2020 01:17:35 +0200 Subject: [PATCH] WIP --- notes.md | 13 +++++++++ src/contexts/index.js | 9 +++++++ src/controls/button-set/index.jsx | 36 +++++++++++++++++++++++++ src/controls/button-set/style.css | 11 ++++++++ src/controls/button/index.jsx | 33 +++++++++++++++++++---- src/controls/button/style.css | 44 +++++++++++++++++++++++++++++++ src/controls/list-item/index.jsx | 2 +- src/controls/menu-item/index.jsx | 9 +++---- src/controls/ribbon/style.css | 14 ++++++++++ src/controls/shared.css | 11 ++++++++ src/index.js | 2 ++ src/themes/dark.css | 29 +++++++++++++++++--- src/util/compose-handlers.js | 11 ++++++++ src/util/themeable/index.jsx | 8 +++--- src/util/use-id.js | 8 ++++++ 15 files changed, 222 insertions(+), 18 deletions(-) create mode 100644 src/contexts/index.js create mode 100644 src/controls/button-set/index.jsx create mode 100644 src/controls/button-set/style.css create mode 100644 src/controls/button/style.css create mode 100644 src/util/compose-handlers.js create mode 100644 src/util/use-id.js diff --git a/notes.md b/notes.md index 62f9d5c..85ab6b2 100644 --- a/notes.md +++ b/notes.md @@ -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 diff --git a/src/contexts/index.js b/src/contexts/index.js new file mode 100644 index 0000000..1ba88cd --- /dev/null +++ b/src/contexts/index.js @@ -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() +}; diff --git a/src/controls/button-set/index.jsx b/src/controls/button-set/index.jsx new file mode 100644 index 0000000..8c08643 --- /dev/null +++ b/src/controls/button-set/index.jsx @@ -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 ( + + + {children} + + + ); + } else { + return children; + } + }); + + return ( +
+ {wrappedChildren} +
+ ); +}; diff --git a/src/controls/button-set/style.css b/src/controls/button-set/style.css new file mode 100644 index 0000000..df14afd --- /dev/null +++ b/src/controls/button-set/style.css @@ -0,0 +1,11 @@ +.buttonSet { + composes: combinedButton from "../shared.css"; +} + +.direction-horizontal { + grid-auto-columns: 1fr; +} + +.direction-vertical { + grid-auto-rows: 1fr; +} diff --git a/src/controls/button/index.jsx b/src/controls/button/index.jsx index 5c8cb91..d6d8206 100644 --- a/src/controls/button/index.jsx +++ b/src/controls/button/index.jsx @@ -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 ( - ); diff --git a/src/controls/button/style.css b/src/controls/button/style.css new file mode 100644 index 0000000..899177b --- /dev/null +++ b/src/controls/button/style.css @@ -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 */ diff --git a/src/controls/list-item/index.jsx b/src/controls/list-item/index.jsx index f074799..82e31f6 100644 --- a/src/controls/list-item/index.jsx +++ b/src/controls/list-item/index.jsx @@ -66,7 +66,7 @@ module.exports = function ListItem({ label, id, children }) { return ( {/* FIXME: item_selected capitalization */} -
+
{(children != null) ? : } diff --git a/src/controls/menu-item/index.jsx b/src/controls/menu-item/index.jsx index e5c68dc..0fa0869 100644 --- a/src/controls/menu-item/index.jsx +++ b/src/controls/menu-item/index.jsx @@ -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 ( diff --git a/src/controls/ribbon/style.css b/src/controls/ribbon/style.css index 0db15c9..a6ec59f 100644 --- a/src/controls/ribbon/style.css +++ b/src/controls/ribbon/style.css @@ -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%; + } } diff --git a/src/controls/shared.css b/src/controls/shared.css index c7d8fde..d7bf008 100644 --- a/src/controls/shared.css +++ b/src/controls/shared.css @@ -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; + } +} diff --git a/src/index.js b/src/index.js index aad939e..74474fb 100644 --- a/src/index.js +++ b/src/index.js @@ -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 */ diff --git a/src/themes/dark.css b/src/themes/dark.css index 0842a7c..810372c 100644 --- a/src/themes/dark.css +++ b/src/themes/dark.css @@ -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; + } } diff --git a/src/util/compose-handlers.js b/src/util/compose-handlers.js new file mode 100644 index 0000000..872b8ad --- /dev/null +++ b/src/util/compose-handlers.js @@ -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); + } + }; +}; diff --git a/src/util/themeable/index.jsx b/src/util/themeable/index.jsx index d6cb2a7..0d4a990 100644 --- a/src/util/themeable/index.jsx +++ b/src/util/themeable/index.jsx @@ -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 [ diff --git a/src/util/use-id.js b/src/util/use-id.js new file mode 100644 index 0000000..e772207 --- /dev/null +++ b/src/util/use-id.js @@ -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()); +};