carbon-components-svelte/src/ContextMenu/ContextMenuOption.svelte
Eric Liu e51f50da0c
Align v10.38 (#698)
* chore(deps-dev): upgrade carbon-components to v10.38.0

* feat(text-input): support read-only variant

* docs: use consistent lingo for inline variant examples

* docs(popover): add Popover alignment example

* fix(file-uploader): adjust markup to avoid accessibility errors

Ref: 0dfde60e3

* fix(inline-loading): use error filled icon

* fix(inline-loading): render iconDescription as title in error/warning icons

Ref: 51c53c923

* fix(structured-list): update accessibility attributes

* fix(tooltip-definition): use span instead of div

Ref: cb6de3025

* fix(multi-select): close menu when blurring the last filterable option

* fix(multi-select): open/focus field for filterable multiselect #635

* fix(multi-select): unblock focus when blurring input #635

* fix(combo-box): select correct item with keys, allow input after clearing #195

* fix(combo-box): update input text if item is selected

* feat(combo-box): render checkmark icon for selected item

* fix(ui-shell): toggle SideNav rail when clicking the hamburger menu #699

* fix(context-menu): update context menu classes #684

* docs(context-menu): improve demo instructions #684

* fix(context-menu): close menu when clicking anywhere
2021-06-27 08:46:57 -07:00

282 lines
7.4 KiB
Svelte

<script>
/**
* Specify the kind of option
* @type {"default" | "danger"}
*/
export let kind = "default";
/** Set to `true` to enable the disabled state */
export let disabled = false;
/** Set to `true` to indent the label */
export let indented = false;
/**
* Specify the icon from `carbon-icons-svelte` to render
* Icon is rendered to the left of the label text
* @type {typeof import("carbon-icons-svelte").CarbonIcon}
*/
export let icon = undefined;
/**
* Specify the label text
* Alternatively, use the "labelText" slot (e.g., <span slot="labelText">...</span>)
*/
export let labelText = "";
/** Set to `true` to use the selected variant */
export let selected = false;
/**
* Set to `true` to enable the selectable variant
* Automatically set to `true` if `selected` is `true`
*/
export let selectable = false;
/**
* Specify the shortcut text
* Alternatively, use the "shortcutText" slot (e.g., <span slot="shortcutText">...</span>)
*/
export let shortcutText = "";
/**
* Specify the id
* It's recommended to provide an id as a value to bind to within a selectable/radio menu group
*/
export let id = "ccs-" + Math.random().toString(36);
/** Obtain a reference to the list item HTML element */
export let ref = null;
import { onMount, getContext, createEventDispatcher, tick } from "svelte";
import ContextMenu from "./ContextMenu.svelte";
import Checkmark16 from "carbon-icons-svelte/lib/Checkmark16/Checkmark16.svelte";
import CaretRight16 from "carbon-icons-svelte/lib/CaretRight16/CaretRight16.svelte";
const dispatch = createEventDispatcher();
const ctx = getContext("ContextMenu");
const ctxGroup = getContext("ContextMenuGroup");
const ctxRadioGroup = getContext("ContextMenuRadioGroup");
// "moderate-01" duration (ms) from Carbon motion recommended for small expansion, short distance movements
const moderate01 = 150;
let unsubCurrentIds = undefined;
let unsubCurrentId = undefined;
let timeoutHover = undefined;
let rootMenuPosition = [0, 0];
let focusIndex = 0;
let options = [];
let role = "menuitem";
let submenuOpen = false;
let submenuPosition = [0, 0];
let menuOffsetX = 0;
const unsubPosition = ctx.position.subscribe((position) => {
rootMenuPosition = position;
});
const unsubMenuOffsetX = ctx.menuOffsetX.subscribe((_menuOffsetX) => {
menuOffsetX = _menuOffsetX;
});
function handleClick(opts = {}) {
if (disabled) return ctx.close();
if (subOptions) return;
if (!!ctxGroup) {
ctxGroup.toggleOption({ id });
} else if (!!ctxRadioGroup) {
if (opts.fromKeyboard) {
ctxRadioGroup.setOption({ id: opts.id });
} else {
ctxRadioGroup.setOption({ id });
}
} else {
selected = !selected;
}
ctx.close();
dispatch("click");
}
onMount(() => {
if (selected === true) selectable = true;
if (ctxGroup) {
unsubCurrentIds = ctxGroup.currentIds.subscribe((_currentIds) => {
selected = _currentIds.includes(id);
});
}
if (ctxRadioGroup) {
unsubCurrentId = ctxRadioGroup.currentId.subscribe((_id) => {
selected = id === _id;
});
}
return () => {
unsubPosition();
unsubMenuOffsetX();
if (unsubCurrentIds) unsubCurrentIds();
if (unsubCurrentId) unsubCurrentId();
if (typeof timeoutHover === "number") clearTimeout(timeoutHover);
};
});
$: isSelectable = !!ctxGroup || selectable;
$: isRadio = !!ctxRadioGroup;
$: subOptions = $$slots.default;
$: ctx.setPopup(submenuOpen);
$: if (submenuOpen) {
const { width, y } = ref.getBoundingClientRect();
let x = rootMenuPosition[0] + width;
if (window.innerWidth - menuOffsetX < width) {
x = rootMenuPosition[0] - width;
}
submenuPosition = [x, y];
}
$: {
if (isSelectable) {
indented = true;
role = "menuitemcheckbox";
if (selected) {
if (ctxGroup) ctxGroup.addOption({ id });
icon = Checkmark16;
} else {
icon = undefined;
}
}
if (isRadio) {
indented = true;
role = "menuitemradio";
ctxRadioGroup.addOption({ id });
if (selected) {
if (ctxRadioGroup) ctxRadioGroup.setOption({ id });
icon = Checkmark16;
} else {
icon = undefined;
}
}
}
</script>
<li
bind:this="{ref}"
role="{role}"
tabindex="-1"
aria-disabled="{!subOptions && disabled}"
aria-haspopup="{subOptions ? true : undefined}"
aria-expanded="{subOptions ? submenuOpen : undefined}"
class:bx--menu-option="{true}"
class:bx--menu-option--disabled="{true}"
class:bx--menu-option--active="{subOptions && submenuOpen}"
class:bx--menu-option--danger="{!subOptions && kind === 'danger'}"
indented="{indented}"
aria-checked="{isSelectable || isRadio ? selected : undefined}"
data-nested="{ref &&
ref.closest('.bx--menu').getAttribute('data-level') === '2'}"
data-sub="{subOptions}"
data-id="{id}"
{...$$restProps}
on:keydown
on:keydown="{async ({ key, target }) => {
if (
subOptions &&
(key === 'ArrowRight' || key === ' ' || key === 'Enter')
) {
submenuOpen = true;
await tick();
options = [...ref.querySelectorAll('li[tabindex]')];
if (options[focusIndex]) options[focusIndex].focus();
return;
}
if (submenuOpen) {
if (key === 'ArrowLeft') {
submenuOpen = false;
focusIndex = 0;
return;
}
if (key === 'ArrowDown') {
if (focusIndex < options.length - 1) focusIndex++;
} else if (key === 'ArrowUp') {
if (focusIndex === -1) {
focusIndex = options.length - 1;
} else {
if (focusIndex > 0) focusIndex--;
}
}
if (options[focusIndex]) options[focusIndex].focus();
}
if (key === ' ' || key === 'Enter') {
handleClick({ fromKeyboard: true, id: target.getAttribute('data-id') });
}
}}"
on:mouseenter
on:mouseenter="{() => {
if (subOptions) {
timeoutHover = setTimeout(() => {
submenuOpen = true;
}, moderate01);
}
}}"
on:mouseleave
on:mouseleave="{(e) => {
if (subOptions) {
if (typeof timeoutHover === 'number') clearTimeout(timeoutHover);
submenuOpen = false;
}
}}"
on:click="{handleClick}"
>
{#if subOptions}
<div
class:bx--menu-option__content="{true}"
class:bx--menu-option__content--disabled="{disabled}"
>
{#if indented}
<div class:bx--menu-option__icon="{true}">
<svelte:component this="{icon}" />
</div>
{/if}
<span class:bx--menu-option__label="{true}" title="{labelText}">
<slot name="labelText">{labelText}</slot>
</span>
<div class:bx--menu-option__info="{true}"><CaretRight16 /></div>
</div>
<ContextMenu
open="{submenuOpen}"
x="{submenuPosition[0]}"
y="{submenuPosition[1]}"
>
<slot />
</ContextMenu>
{:else}
<div
class:bx--menu-option__content="{true}"
class:bx--menu-option__content--disabled="{disabled}"
>
{#if indented}
<div class:bx--menu-option__icon="{true}">
<svelte:component this="{icon}" />
</div>
{/if}
<span class:bx--menu-option__label="{true}" title="{labelText}">
<slot name="labelText">{labelText}</slot>
</span>
<div class:bx--menu-option__info="{true}">
<slot name="shortcutText">{shortcutText}</slot>
</div>
</div>
{/if}
</li>