React context menu system
npm install @oversword/contextContext component will create an element for you, passing the children and attributes given to the Context component onto the created element.``jsx`
Child
Components!
`
Results in:jsx`
Child
Components!
component will not create an element, and should be used only as a wrapper for other elements. The children will be rendered as normal, unaffected.
`jsx
Child
Components!
`
Results in:
`jsx
Child
Components!
`
Configuration
Typically, to make use of the context functionality, you will want to pass one or more of these properties to the Context or DataContext components.Making a context will attach the it to the context tree, with the context containing it becoming the parent. Events will be bubbled up this tree, and configuration will be passed down from this tree.
To avoid this, mark it with the
root flag. This Context will become the root of its own tree.
`jsx
Contents
Contents
`$3
The context can be passed in as context - or by the name of the component: Context and DataContext respectively, for special reasons which will not be justified here.* The context is an object which may contain any of the following properties as needed.
#### Context Type
Though not required, the most basic context would only define its own type.
`jsx
const context = {
type: 'my-label-type',
}
Contents
/ OR /
Contents
`acts property, which will define which actions are possible for this type.
`jsx
const context = {
type: 'my-label-type',
acts: {
'some-action': {},
'another-action': {},
}
}
Contents
/ OR /
Contents
`You can override
acts in parent contexts
`jsx
const wrapperContext = {
overrides: {
'my-label-type': {
acts: {
'override-action': {}
}
}
}
}
const context = {
type: 'my-label-type',
acts: {
'some-action': {},
'another-action': {
disabled: () => Boolean(Math.round(Math.random()))
},
}
}
Contents
`##### Context Acts Conditions
Acts can have conditions configured, which will determine whether or not they are allowed to fire.
* If the
condition property is false, or returns false on evaluation, the action will not exist on the context, and will not be triggerable. Any attempt to trigger this action will result in the UNHANDLED Symbol.
* If the disabled property is true, or returns true on evaluation, the action will exist, but will not trigger. Triggering this action will result in the HANDLED symbol, preventing other contexts from intercepting the action, and will not trigger the handlers associated with the action.
`jsx
const context = {
type: 'my-label-type',
acts: {
'some-action': {
condition: ({ type, action, path, data, event }) =>
Boolean(Math.round(Math.random()))
},
'another-action': {
disabled: ({ type, action, path, data, event }) =>
Boolean(Math.round(Math.random()))
},
}
}
Contents
/ OR /
Contents
``jsx
const context = {
type: 'my-label-type',
acts: {
'some-action': {
keys: ['Click']
},
'another-action': {},
},
}
Contents
`You can also override
keys in parent contexts
`jsx
const wrapperContext = {
overrides: {
'my-label-type': {
acts: {
'some-action': {
keys: ['Click','Enter']
}
}
}
}
}
const context = {
type: 'my-label-type',
acts: {
'some-action': {
keys: ['Ctrl+L']
},
'another-action': {},
},
menu: [
{
action: 'some-action',
label: 'Trigger Some Action',
},
{
action: 'another-action',
label: 'Do Another Action',
}
],
}
Contents
`menu property, which will define the display of the context menu.
* The acts each menu item references must be configured for the specific type. If the action referenced does not exist, the menu item will not display in the menu at all.`jsx
const context = {
type: 'my-label-type',
acts: {
'some-action': {},
'another-action': {},
},
menu: [
{
action: 'some-action',
label: 'Trigger Some Action',
},
{
action: 'another-action',
label: 'Do Another Action',
}
]
}
Contents
`You can override
menu in parent contexts
`jsx
const wrapperContext = {
overrides: {
'my-label-type': {
menu: [
{
action: 'override-action',
label: 'Overriding Menu Item'
}
]
}
}
}
const context = {
type: 'my-label-type',
acts: {
'some-action': {},
'another-action': {},
},
menu: [
{
action: 'some-action',
label: 'Trigger Some Action',
},
{
action: 'another-action',
label: 'Do Another Action',
}
]
}
Contents
`Menu items can be grouped by using the "section" mode, and providing children
`jsx
const context = {
type: 'my-label-type',
acts: {
'some-action': {},
'another-action': {},
},
menu: [
{
action: 'some-action',
label: 'Trigger Some Action',
},
{
label: 'Action List',
mode: 'section',
children: [
{
action: 'another-action',
label: 'Do Another Action',
}
]
}
]
}
Contents
`Menus can be nested by using the "branch" mode, and providing children
`jsx
const context = {
type: 'my-label-type',
acts: {
'some-action': {},
'another-action': {},
},
menu: [
{
action: 'some-action',
label: 'Trigger Some Action',
},
{
label: 'Additional Actions...',
mode: 'branch',
children: [
{
action: 'another-action',
label: 'Do Another Action',
}
]
}
]
}
Contents
`$3
Data an be provided to a context, which will become the data available on the action object. This is where you can provide contextual data that will be used when handling the action.The data should be a normal object with string keys, it may be single-level merged with other data under this assumption. The types of values associated with those keys will not affect the system.
`jsx
MyContext_info: 'something-important',
}} >
Contents
`Using a data-only context is an obvious use-case for a
DataContext component, this example also removes the burden of the sub-component holding the key metadata.The data given here can later be used to identify the source component's model.
`jsx
{someList.map(({ label, key }) => (
key={key}
data={{
MyContext_info: 'something-important',
MyContext_key: key,
}} >
))}
`The data can also be provided by a generator function, this can also be used for custom merging strategies if the default merging strategy is not desired. The generator will not be evaluated until an action is triggered.
`jsx
({
...currentData,
Existing_key: undefined,
MyContext_info: 'something-important',
})} >
Contents
`$3
An intercept property can be provided to the context in order to catch and handle actions bubbling up from child components.`jsx
const subComponentContext = {
type: 'sub-component',
acts: {
'some-action': {
keys: ['Click','Enter']
}
},
}
const SubComponent = ({ name }) => (
{name}
)const parentIntercept = {
'sub-component.some-action': ({ type, path, action, data, event }) => {
console.log(data)
/*
{
MyContext_info: 'something-important',
MyContext_key: key,
}
*/
}
}
{someList.map(({ label, key }) => (
key={key}
data={{
MyContext_info: 'something-important',
MyContext_key: key,
}} >
))}
`Intercepts can also be defined as other actions, triggering them from the intercepting context.
`jsx
const subComponentContext = {
type: 'sub-component',
acts: {
'some-action': {
keys: ['Click','Enter']
}
},
}
const SubComponent = ({ name }) => (
{name}
)const parentContext = {
type: 'parent-component',
acts: {
'parent-action': {}
},
}
const parentIntercept = {
'sub-component.some-action': 'parent-action'
}
{someList.map(({ label, key }) => (
key={key}
data={{
MyContext_info: 'something-important',
MyContext_key: key,
}} >
))}
`outercept property will be effectively the same as the intercept property, except that action priorities will "bubble down". Everything else should behave as if the action has "bubbled up", but the order in which actions are caught will start at the root ancestor and work down (the opposite of intercept), allowing you to completely override descendant behaviour from any ancestor context.Outercepts will always be prioritised over intercepts unless the outercepts go unhandled.
`jsx
const subComponentContext = {
type: 'sub-component',
acts: {
'some-action': {
keys: ['Click','Enter']
}
},
}
const subComponentIntercept = {
'sub-component.some-action': ({ type, path, action, data, event }) => {
// Handles it's own action?
}
}
const SubComponent = ({ name }) => (
{name}
)const parentOutercept = {
'sub-component.some-action': ({ type, path, action, data, event }) => {
// Action is overriden by ancestor!
// Descendant handler will never call
}
}
{someList.map(({ label, key }) => (
key={key}
data={{
MyContext_info: 'something-important',
MyContext_key: key,
}} >
))}
`
* Note this outercept feature is overpowered, ripe for contraversy, and may one day be removedContext Component Properties
The following properties should only be passed to the Context component, as they all imply the existence of an element the context is associated with.
This is not true for the DataContext component, which should only (but always) be used when there is no associated element.$3
element
The component that is rendered can be customised using the element property.
`jsx
Button Label
`
Results in:
`jsx
`You can also use existing React components, passing down any properties in the same way as attributes.
`jsx
Component Contents
`
Results in:
`jsx
Component Contents
`focus
You can make the element unfocussable by switching off the focus property, which is true by default.
`jsx
Contents
`
Results in:
`jsx
Contents
`tabIndex
You can customise the focus order of elements by passing in the tabIndex property, this will be 0 by default.
`jsx
Contents
`
Results in:
`jsx
Contents
`$3
This feature is redundant, and not recomended. You should configure mouse events to trigger actions by configuring context keys in the same way as keyboard "buttons". Valid mouse "keys" are Click, DoubleClick, Button1, Button2, Button3, Button4, Button5.These properties can be used to bind actions directly to the rendered element events. When the event occurs, the action will be triggered.
`
onChangeAction
onClickAction
onDoubleClickAction
onMouseDownAction
onMouseUpAction
onMouseMoveAction
`The action can be defined as:
* A string, the name of the action to be triggered
onDoubleClickAction="action-name"
* A function, returning the name of the action. Any existing iformation about the action will be passed as the first argument, including the source event.
onClickAction={({ data, type, path, event }) => 'action-name'}
* An object, containing a condition property and action property, which may itself be a string or a function.
* Using simple data:
`
onMouseDownAction={{
condition: someVar > 7,
action: 'action-name'
}}
`
* Using generative functions (will be evaluated only when the event is triggered):
`
onMouseDownAction={{
condition: ({ data, type, path, event }) =>
Boolean(event.target.closest('.target-element')),
action: ({ data, type, path, event }) =>
'action-name'
}}
`Property Behaviours
Properties that will be intercepted, and not passed through to the rendered component:
`
root
focus
element
Context/context
data
intercept
outercept
onClickAction
onDoubleClickAction
onMouseDownAction
onMouseUpAction
onMouseMoveAction
`Properties that may be modfied before passing them through to the rendered component:
`
tabIndex
onFocus
className
ref
`Properties that will be overridden, even if they are passed through to the rendered component:
`
apiRef
data-contextidonContextMenu (if the Context is focussable)
onClick (if onClickAction is provided)
onDoubleClick (if onDoubleClickAction is provided)
onMouseMove (if onMouseMoveAction is provided)
onMouseDown (if onMouseDownAction is provided)
onMouseUp (if onMouseUpAction is provided)
`Valid Keys
These keys, "buttons" or "symbols" will be available for configuring the keys property of a context, and can all be used in combination with each other to define key combinations that trigger actions.* Thas has all been configured with UK Mac keyboards in mind, and is not customisable yet, behaviour on other keyboards may be unoptimal.
$3
`
Click
DoubleClick
Button1 (left mouse button)
Button2 (middle mouse button, press scroll button)
Button3 (right mouse button)
Button4 (uncommon; scrolling up/down or browser back/forth)
Button5 (uncommon; scrolling up/down or browser back/forth)
`$3
These symbols will not depend on positional key information e.g. CtrlLeft/CtrlRight`
Ctrl
Shift
CapsLock
Meta (windows key, or command on mac)
Alt (option on mac)Enter
Space
Tab
Delete
Backspace
ArrowLeft
ArrowRight
ArrowUp
ArrowDown
IntlBackslash (unadvised: the position, appearance, and expected behaviour of this key are not predictable between keyboards)
`
* Note the Function (fn) key cannot be intercepted at the browser level at all. Symbols that require the Function key to be accessed will report as if they are actually that symbol, and will contain no information about the symbol it would have been without the Function key being pressed.`
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
``
F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12
``
1 2 3 4 5 6 7 8 9 0
`#### The symbols which can be accessed without holding shift
`
- = [ ] ; ' \ , . /`
+ *
``