mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-17 11:11:25 +00:00
Alignment with Carbon version 10.31 (#571)
* chore(deps-dev): upgrade carbon-components to v10.31.0 * fix(slider): use CSS to hide input if hideTextInput is true * docs(slider): add hidden text input, invalid, disabled examples * feat(tabs): support "container" type for TabsSkeleton * chore(list-box): remove hotfix inline style to center dropdown chevron * fix(number-input): use add, subtract icons and update markup * feat(select): add warning state * docs(select): add invalid state example * docs(select): add helper text example * fix(structured-list): add "rowgroup" role to StructuredListBody * docs: release code snippet max-width * docs(select): add skeleton hidden label example * feat(popover): add Popover component * feat(pagination): dispatch button click events to be consistent with PaginationNav * fix(multi-select): type clear as a custom event * docs(radio-button): add disabled buttons example * chore(tabs): use absolute icon import * fix(link): remove line breaks within anchor link * docs(radio-button): adjust section copy verbiage * chore(deps-dev): upgrade carbon-icons-svelte to v10.27 v10.27 uses the SvelteComponentTyped interface * docs(accordion): adjust section title verbiage * test(types): fix warnings from svelte-check * fix(search): only set autofocus attribute if equals true * feat(popover): add closeOnOutsideClick prop * docs: style [data-outline] as relative positioned * feat(context-menu): add initial ContextMenu * feat(context-menu): annotate props, generate types * feat(context-menu): add initial focus logic * fix(context-menu): correctly tab in/out of nested menus * chore(context-menu): update types * fix(context-menu): obtain radio id from node directly * docs(context-menu): add examples and test * fix(context-menu): prevent default keydown behavior
This commit is contained in:
parent
afed4fa2fa
commit
5fad0cb3c7
52 changed files with 1758 additions and 103 deletions
130
src/ContextMenu/ContextMenu.svelte
Normal file
130
src/ContextMenu/ContextMenu.svelte
Normal file
|
@ -0,0 +1,130 @@
|
|||
<script>
|
||||
/**
|
||||
* Set to `true` to open the menu
|
||||
* Either `x` and `y` must be greater than zero
|
||||
*/
|
||||
export let open = false;
|
||||
|
||||
/** Specify the horizontal offset of the menu position */
|
||||
export let x = 0;
|
||||
|
||||
/** Specify the vertical offset of the menu position */
|
||||
export let y = 0;
|
||||
|
||||
/** Obtain a reference to the unordered list HTML element */
|
||||
export let ref = null;
|
||||
|
||||
import {
|
||||
setContext,
|
||||
getContext,
|
||||
afterUpdate,
|
||||
createEventDispatcher,
|
||||
} from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const position = writable([x, y]);
|
||||
const currentIndex = writable(-1);
|
||||
const hasPopup = writable(false);
|
||||
const ctx = getContext("ContextMenu");
|
||||
|
||||
let options = [];
|
||||
let direction = 1;
|
||||
let prevX = 0;
|
||||
let prevY = 0;
|
||||
let focusIndex = -1;
|
||||
|
||||
function close() {
|
||||
open = false;
|
||||
x = 0;
|
||||
y = 0;
|
||||
prevX = 0;
|
||||
prevY = 0;
|
||||
focusIndex = -1;
|
||||
}
|
||||
|
||||
setContext("ContextMenu", {
|
||||
currentIndex,
|
||||
position,
|
||||
close,
|
||||
setPopup: (popup) => {
|
||||
hasPopup.set(popup);
|
||||
},
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
if (open) {
|
||||
options = [...ref.querySelectorAll("li[data-nested='false']")];
|
||||
|
||||
if (level === 1) {
|
||||
if (prevX !== x || prevY !== y) ref.focus();
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
}
|
||||
|
||||
dispatch("open");
|
||||
} else {
|
||||
dispatch("close");
|
||||
}
|
||||
|
||||
if (!$hasPopup && options[focusIndex]) options[focusIndex].focus();
|
||||
});
|
||||
|
||||
$: level = !ctx ? 1 : 2;
|
||||
$: currentIndex.set(focusIndex);
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:contextmenu|preventDefault="{(e) => {
|
||||
if (level > 1) return;
|
||||
if (open || x === 0) x = e.x;
|
||||
if (open || y === 0) y = e.y;
|
||||
position.set([x, y]);
|
||||
open = true;
|
||||
}}"
|
||||
on:click="{(e) => {
|
||||
if (!open) return;
|
||||
if (e.target.contains(ref)) close();
|
||||
}}"
|
||||
on:keydown="{(e) => {
|
||||
if (open && e.key === 'Escape') close();
|
||||
}}"
|
||||
/>
|
||||
|
||||
<ul
|
||||
bind:this="{ref}"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
data-direction="{direction}"
|
||||
data-level="{level}"
|
||||
class:bx--context-menu="{true}"
|
||||
class:bx--context-menu--open="{open}"
|
||||
class:bx--context-menu--invisible="{open && x === 0 && y === 0}"
|
||||
class:bx--context-menu--root="{level === 1}"
|
||||
{...$$restProps}
|
||||
style="left: {x}px; top: {y}px; {$$restProps.style}"
|
||||
on:click
|
||||
on:click="{({ target }) => {
|
||||
const closestOption = target.closest('[tabindex]');
|
||||
|
||||
if (closestOption && closestOption.getAttribute('role') !== 'menuitem') {
|
||||
close();
|
||||
}
|
||||
}}"
|
||||
on:keydown
|
||||
on:keydown|preventDefault="{(e) => {
|
||||
if ($hasPopup) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
if (focusIndex < options.length - 1) focusIndex++;
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
if (focusIndex === -1) {
|
||||
focusIndex = options.length - 1;
|
||||
} else {
|
||||
if (focusIndex > 0) focusIndex--;
|
||||
}
|
||||
}
|
||||
}}"
|
||||
>
|
||||
<slot />
|
||||
</ul>
|
1
src/ContextMenu/ContextMenuDivider.svelte
Normal file
1
src/ContextMenu/ContextMenuDivider.svelte
Normal file
|
@ -0,0 +1 @@
|
|||
<li role="separator" class:bx--context-menu-divider="{true}"></li>
|
36
src/ContextMenu/ContextMenuGroup.svelte
Normal file
36
src/ContextMenu/ContextMenuGroup.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
/** @type {string[]} */
|
||||
export let selectedIds = [];
|
||||
|
||||
/** Specify the label text */
|
||||
export let labelText = "";
|
||||
|
||||
import { setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
const currentIds = writable([]);
|
||||
|
||||
setContext("ContextMenuGroup", {
|
||||
currentIds,
|
||||
addOption: ({ id }) => {
|
||||
if (!selectedIds.includes(id)) {
|
||||
selectedIds = [...selectedIds, id];
|
||||
}
|
||||
},
|
||||
toggleOption: ({ id }) => {
|
||||
if (!selectedIds.includes(id)) {
|
||||
selectedIds = [...selectedIds, id];
|
||||
} else {
|
||||
selectedIds = selectedIds.filter((_) => _ !== id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
$: currentIds.set(selectedIds);
|
||||
</script>
|
||||
|
||||
<li role="none">
|
||||
<ul role="group" aria-label="{labelText}">
|
||||
<slot />
|
||||
</ul>
|
||||
</li>
|
263
src/ContextMenu/ContextMenuOption.svelte
Normal file
263
src/ContextMenu/ContextMenuOption.svelte
Normal file
|
@ -0,0 +1,263 @@
|
|||
<script>
|
||||
/** 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];
|
||||
|
||||
const unsubPosition = ctx.position.subscribe((position) => {
|
||||
rootMenuPosition = position;
|
||||
});
|
||||
|
||||
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();
|
||||
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();
|
||||
submenuPosition = [rootMenuPosition[0] + width, 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--context-menu-option="{true}"
|
||||
class:bx--context-menu-option--disabled="{true}"
|
||||
class:bx--context-menu-option--active="{subOptions && submenuOpen}"
|
||||
indented="{indented}"
|
||||
aria-checked="{isSelectable || isRadio ? selected : undefined}"
|
||||
data-nested="{ref &&
|
||||
ref.closest('.bx--context-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--context-menu-option__content="{true}"
|
||||
class:bx--context-menu-option__content--disabled="{disabled}"
|
||||
>
|
||||
{#if indented}
|
||||
<div class:bx--context-menu-option__icon="{true}">
|
||||
<svelte:component this="{icon}" />
|
||||
</div>
|
||||
{/if}
|
||||
<span class:bx--context-menu-option__label="{true}" title="{labelText}">
|
||||
<slot name="labelText">{labelText}</slot>
|
||||
</span>
|
||||
<div class:bx--context-menu-option__info="{true}"><CaretRight16 /></div>
|
||||
</div>
|
||||
|
||||
<ContextMenu
|
||||
open="{submenuOpen}"
|
||||
x="{submenuPosition[0]}"
|
||||
y="{submenuPosition[1]}"
|
||||
>
|
||||
<slot />
|
||||
</ContextMenu>
|
||||
{:else}
|
||||
<div
|
||||
class:bx--context-menu-option__content="{true}"
|
||||
class:bx--context-menu-option__content--disabled="{disabled}"
|
||||
>
|
||||
{#if indented}
|
||||
<div class:bx--context-menu-option__icon="{true}">
|
||||
<svelte:component this="{icon}" />
|
||||
</div>
|
||||
{/if}
|
||||
<span class:bx--context-menu-option__label="{true}" title="{labelText}">
|
||||
<slot name="labelText">{labelText}</slot>
|
||||
</span>
|
||||
<div class:bx--context-menu-option__info="{true}">
|
||||
<slot name="shortcutText">{shortcutText}</slot>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
34
src/ContextMenu/ContextMenuRadioGroup.svelte
Normal file
34
src/ContextMenu/ContextMenuRadioGroup.svelte
Normal file
|
@ -0,0 +1,34 @@
|
|||
<script>
|
||||
/** Set the selected radio group id */
|
||||
export let selectedId = "";
|
||||
|
||||
/** Specify the label text */
|
||||
export let labelText = "";
|
||||
|
||||
import { setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
const currentId = writable("");
|
||||
const radioIds = writable([]);
|
||||
|
||||
setContext("ContextMenuRadioGroup", {
|
||||
currentId,
|
||||
radioIds,
|
||||
addOption: ({ id }) => {
|
||||
if (!$radioIds.includes(id)) {
|
||||
radioIds.update((_) => [..._, id]);
|
||||
}
|
||||
},
|
||||
setOption: ({ id }) => {
|
||||
selectedId = id;
|
||||
},
|
||||
});
|
||||
|
||||
$: currentId.set(selectedId);
|
||||
</script>
|
||||
|
||||
<li role="none">
|
||||
<ul role="group" aria-label="{labelText}">
|
||||
<slot />
|
||||
</ul>
|
||||
</li>
|
5
src/ContextMenu/index.js
Normal file
5
src/ContextMenu/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export { default as ContextMenu } from "./ContextMenu.svelte";
|
||||
export { default as ContextMenuDivider } from "./ContextMenuDivider.svelte";
|
||||
export { default as ContextMenuGroup } from "./ContextMenuGroup.svelte";
|
||||
export { default as ContextMenuOption } from "./ContextMenuOption.svelte";
|
||||
export { default as ContextMenuRadioGroup } from "./ContextMenuRadioGroup.svelte";
|
|
@ -40,7 +40,6 @@
|
|||
<slot />
|
||||
</p>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a
|
||||
bind:this="{ref}"
|
||||
class:bx--link="{true}"
|
||||
|
@ -55,8 +54,6 @@
|
|||
on:click
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mouseleave><slot /></a
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
{/if}
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
class:bx--list-box__menu-icon--open="{open}"
|
||||
{...$$restProps}
|
||||
on:click|preventDefault
|
||||
style="top: 0; bottom: 0; margin: auto;"
|
||||
>
|
||||
<ChevronDown16 aria-label="{description}" title="{description}" />
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
<script>
|
||||
/**
|
||||
* @typedef {string} MultiSelectItemId
|
||||
* @typedef {string} MultiSelectItemText
|
||||
* @typedef {{ id: MultiSelectItemId; text: MultiSelectItemText; }} MultiSelectItem
|
||||
* @event {{ selectedIds: string[]; selected: MultiSelectItem[]; unselected: MultiSelectItem[]; }} select
|
||||
* @event {any} clear
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the multiselect items
|
||||
* @type {MultiSelectItem[]}
|
||||
|
@ -117,13 +125,6 @@
|
|||
*/
|
||||
export let name = undefined;
|
||||
|
||||
/**
|
||||
* @typedef {string} MultiSelectItemId
|
||||
* @typedef {string} MultiSelectItemText
|
||||
* @typedef {{ id: MultiSelectItemId; text: MultiSelectItemText; }} MultiSelectItem
|
||||
* @event {{ selectedIds: string[]; selected: MultiSelectItem[]; unselected: MultiSelectItem[]; }} select
|
||||
*/
|
||||
|
||||
import { afterUpdate, createEventDispatcher, setContext } from "svelte";
|
||||
import WarningFilled16 from "carbon-icons-svelte/lib/WarningFilled16/WarningFilled16.svelte";
|
||||
import WarningAltFilled16 from "carbon-icons-svelte/lib/WarningAltFilled16/WarningAltFilled16.svelte";
|
||||
|
|
|
@ -101,8 +101,8 @@
|
|||
export let ref = null;
|
||||
|
||||
import { createEventDispatcher, afterUpdate } from "svelte";
|
||||
import CaretDownGlyph from "carbon-icons-svelte/lib/CaretDownGlyph/CaretDownGlyph.svelte";
|
||||
import CaretUpGlyph from "carbon-icons-svelte/lib/CaretUpGlyph/CaretUpGlyph.svelte";
|
||||
import Add16 from "carbon-icons-svelte/lib/Add16/Add16.svelte";
|
||||
import Subtract16 from "carbon-icons-svelte/lib/Subtract16/Subtract16.svelte";
|
||||
import WarningFilled16 from "carbon-icons-svelte/lib/WarningFilled16/WarningFilled16.svelte";
|
||||
import WarningAltFilled16 from "carbon-icons-svelte/lib/WarningAltFilled16/WarningAltFilled16.svelte";
|
||||
|
||||
|
@ -187,7 +187,7 @@
|
|||
}}"
|
||||
disabled="{disabled}"
|
||||
>
|
||||
<CaretDownGlyph class="down-icon" />
|
||||
<Subtract16 class="down-icon" />
|
||||
</button>
|
||||
<input
|
||||
bind:this="{ref}"
|
||||
|
@ -221,7 +221,7 @@
|
|||
}}"
|
||||
disabled="{disabled}"
|
||||
>
|
||||
<CaretUpGlyph class="up-icon" />
|
||||
<Add16 class="up-icon" />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
|
@ -271,23 +271,7 @@
|
|||
<div class:bx--number__controls="{true}">
|
||||
<button
|
||||
type="button"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
title="{incrementLabel || iconDescription}"
|
||||
aria-label="{incrementLabel || iconDescription}"
|
||||
class:bx--number__control-btn="{true}"
|
||||
class:up-icon="{true}"
|
||||
on:click="{() => {
|
||||
updateValue(1);
|
||||
}}"
|
||||
disabled="{disabled}"
|
||||
>
|
||||
<CaretUpGlyph class="up-icon" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
tabindex="-1"
|
||||
title="{decrementLabel || iconDescription}"
|
||||
aria-label="{decrementLabel || iconDescription}"
|
||||
class:bx--number__control-btn="{true}"
|
||||
|
@ -297,8 +281,24 @@
|
|||
}}"
|
||||
disabled="{disabled}"
|
||||
>
|
||||
<CaretDownGlyph class="down-icon" />
|
||||
<Subtract16 class="down-icon" />
|
||||
</button>
|
||||
<div class:bx--number__rule-divider="{true}"></div>
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
title="{incrementLabel || iconDescription}"
|
||||
aria-label="{incrementLabel || iconDescription}"
|
||||
class:bx--number__control-btn="{true}"
|
||||
class:up-icon="{true}"
|
||||
on:click="{() => {
|
||||
updateValue(1);
|
||||
}}"
|
||||
disabled="{disabled}"
|
||||
>
|
||||
<Add16 class="up-icon" />
|
||||
</button>
|
||||
<div class:bx--number__rule-divider="{true}"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script>
|
||||
/**
|
||||
* @event {{ pageSize: number; page: number; }} update
|
||||
* @event {{ page: number; }} click:button--previous
|
||||
* @event {{ page: number; }} click:button--next
|
||||
*/
|
||||
|
||||
/** Specify the current page index */
|
||||
|
@ -162,6 +164,7 @@
|
|||
: ''}"
|
||||
on:click="{() => {
|
||||
page--;
|
||||
dispatch('click:button--previous', { page });
|
||||
}}"
|
||||
/>
|
||||
<Button
|
||||
|
@ -177,6 +180,7 @@
|
|||
: ''}"
|
||||
on:click="{() => {
|
||||
page++;
|
||||
dispatch('click:button--next', { page });
|
||||
}}"
|
||||
/>
|
||||
</div>
|
||||
|
|
68
src/Popover/Popover.svelte
Normal file
68
src/Popover/Popover.svelte
Normal file
|
@ -0,0 +1,68 @@
|
|||
<script>
|
||||
/** Set to `true` to display the popover */
|
||||
export let open = false;
|
||||
|
||||
/** Set to `true` to close the popover on an outside click */
|
||||
export let closeOnOutsideClick = false;
|
||||
|
||||
/** Set to `true` render a caret */
|
||||
export let caret = false;
|
||||
|
||||
/**
|
||||
* Specify the alignment of the caret
|
||||
* @type {"top" | "top-left" | "top-right" | "bottom" | "bottom-left" | "bottom-right" | "left" | "left-bottom" | "left-top" | "right" | "right-bottom" | "right-top"}
|
||||
*/
|
||||
export let align = "top";
|
||||
|
||||
/** Set to `true` to enable the light variant */
|
||||
export let light = false;
|
||||
|
||||
/** Set to `true` to enable the high contrast variant */
|
||||
export let highContrast = false;
|
||||
|
||||
/** Set to `true` to use a relative position */
|
||||
export let relative = false;
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let ref = null;
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:click="{(e) => {
|
||||
if (!open) return;
|
||||
if (e.target.contains(ref)) {
|
||||
dispatch('click:outside');
|
||||
if (closeOnOutsideClick) open = false;
|
||||
}
|
||||
}}"
|
||||
/>
|
||||
|
||||
<div
|
||||
bind:this="{ref}"
|
||||
class:bx--popover="{true}"
|
||||
class:bx--popover--caret="{caret}"
|
||||
class:bx--popover--light="{light}"
|
||||
class:bx--popover--high-contrast="{highContrast}"
|
||||
class:bx--popover--top="{align === 'top'}"
|
||||
class:bx--popover--top-left="{align === 'top-left'}"
|
||||
class:bx--popover--top-right="{align === 'top-right'}"
|
||||
class:bx--popover--bottom="{align === 'bottom'}"
|
||||
class:bx--popover--bottom-left="{align === 'bottom-left'}"
|
||||
class:bx--popover--bottom-right="{align === 'bottom-right'}"
|
||||
class:bx--popover--left="{align === 'left'}"
|
||||
class:bx--popover--left-bottom="{align === 'left-bottom'}"
|
||||
class:bx--popover--left-top="{align === 'left-top'}"
|
||||
class:bx--popover--right="{align === 'right'}"
|
||||
class:bx--popover--right-bottom="{align === 'right-bottom'}"
|
||||
class:bx--popover--right-top="{align === 'right-top'}"
|
||||
class:bx--popover--open="{open}"
|
||||
class:bx--popover--relative="{relative}"
|
||||
{...$$restProps}
|
||||
>
|
||||
<div class:bx--popover-contents="{true}">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
1
src/Popover/index.js
Normal file
1
src/Popover/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as Popover } from "./Popover.svelte";
|
|
@ -108,7 +108,7 @@
|
|||
bind:this="{ref}"
|
||||
role="searchbox"
|
||||
class:bx--search-input="{true}"
|
||||
autofocus="{autofocus}"
|
||||
autofocus="{autofocus === true ? true : undefined}"
|
||||
autocomplete="{autocomplete}"
|
||||
disabled="{disabled}"
|
||||
id="{id}"
|
||||
|
|
|
@ -39,6 +39,12 @@
|
|||
/** Specify the invalid state text */
|
||||
export let invalidText = "";
|
||||
|
||||
/** Set to `true` to indicate an warning state */
|
||||
export let warn = false;
|
||||
|
||||
/** Specify the warning state text */
|
||||
export let warnText = "";
|
||||
|
||||
/** Specify the helper text */
|
||||
export let helperText = "";
|
||||
|
||||
|
@ -58,6 +64,7 @@
|
|||
import { writable } from "svelte/store";
|
||||
import ChevronDown16 from "carbon-icons-svelte/lib/ChevronDown16/ChevronDown16.svelte";
|
||||
import WarningFilled16 from "carbon-icons-svelte/lib/WarningFilled16/WarningFilled16.svelte";
|
||||
import WarningAltFilled16 from "carbon-icons-svelte/lib/WarningAltFilled16/WarningAltFilled16.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const selectedValue = writable(selected);
|
||||
|
@ -80,6 +87,7 @@
|
|||
class:bx--select--light="{light}"
|
||||
class:bx--select--invalid="{invalid}"
|
||||
class:bx--select--disabled="{disabled}"
|
||||
class:bx--select--warning="{warn}"
|
||||
>
|
||||
{#if !noLabel}
|
||||
<label
|
||||
|
@ -158,6 +166,11 @@
|
|||
{#if invalid}
|
||||
<WarningFilled16 class="bx--select__invalid-icon" />
|
||||
{/if}
|
||||
{#if !invalid && warn}
|
||||
<WarningAltFilled16
|
||||
class="bx--select__invalid-icon bx--select__invalid-icon--warning"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !invalid && helperText}
|
||||
<div
|
||||
|
@ -172,6 +185,11 @@
|
|||
{invalidText}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !invalid && warn}
|
||||
<div id="{errorId}" class:bx--form-requirement="{true}">
|
||||
{warnText}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -191,21 +191,27 @@
|
|||
/>
|
||||
</div>
|
||||
<span class:bx--slider__range-label="{true}">{maxLabel || max}</span>
|
||||
{#if !hideTextInput}
|
||||
<input
|
||||
type="{inputType}"
|
||||
id="input-{id}"
|
||||
aria-label="{$$props['aria-label'] || 'Slider number input'}"
|
||||
class:bx--text-input="{true}"
|
||||
class:bx--slider-text-input="{true}"
|
||||
class:bx--text-input--light="{light}"
|
||||
class:bx--text-input--invalid="{invalid}"
|
||||
on:change="{({ target }) => {
|
||||
value = Number(target.value);
|
||||
}}"
|
||||
disabled="{disabled}"
|
||||
value="{value}"
|
||||
/>
|
||||
{/if}
|
||||
<input
|
||||
type="{hideTextInput ? 'hidden' : inputType}"
|
||||
style="{hideTextInput ? 'display: none' : undefined}"
|
||||
id="input-{id}"
|
||||
name="{name}"
|
||||
class:bx--text-input="{true}"
|
||||
class:bx--slider-text-input="{true}"
|
||||
class:bx--text-input--light="{light}"
|
||||
class:bx--text-input--invalid="{invalid}"
|
||||
value="{value}"
|
||||
aria-label="{$$props['aria-label'] || 'Slider number input'}"
|
||||
disabled="{disabled}"
|
||||
required="{required}"
|
||||
min="{min}"
|
||||
max="{max}"
|
||||
step="{step}"
|
||||
on:change="{({ target }) => {
|
||||
value = Number(target.value);
|
||||
}}"
|
||||
data-invalid="{invalid || null}"
|
||||
aria-invalid="{invalid || null}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<div
|
||||
class:bx--structured-list-tbody="{true}"
|
||||
role="rowgroup"
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:mouseover
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { createEventDispatcher, afterUpdate, setContext } from "svelte";
|
||||
import { writable, derived } from "svelte/store";
|
||||
import ChevronDownGlyph from "carbon-icons-svelte/lib/ChevronDownGlyph";
|
||||
import ChevronDownGlyph from "carbon-icons-svelte/lib/ChevronDownGlyph/ChevronDownGlyph.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
<script>
|
||||
/** Specify the number of tabs to render */
|
||||
export let count = 4;
|
||||
|
||||
/**
|
||||
* Specify the type of tabs
|
||||
* @type {"default" | "container"}
|
||||
*/
|
||||
export let type = "default";
|
||||
</script>
|
||||
|
||||
<div
|
||||
class:bx--tabs="{true}"
|
||||
class:bx--skeleton="{true}"
|
||||
class:bx--tabs--scrollable="{true}"
|
||||
class:bx--tabs--scrollable--container="{type === 'container'}"
|
||||
{...$$restProps}
|
||||
on:click
|
||||
on:mouseover
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
>
|
||||
<div class:bx--tabs-trigger="{true}">
|
||||
<div class:bx--tabs-trigger-text="{true}"> </div>
|
||||
<svg width="10" height="5" viewBox="0 0 10 5" fill-rule="evenodd">
|
||||
<path d="M10 0L5 5 0 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<ul class:bx--tabs__nav="{true}" class:bx--tabs__nav--hidden="{true}">
|
||||
{#each Array.from({ length: count }, (_, i) => i) as item, i (item)}
|
||||
<li class:bx--tabs__nav-item="{true}">
|
||||
<div class:bx--tabs__nav-link="{true}"> </div>
|
||||
<ul class:bx--tabs--scrollable__nav="{true}">
|
||||
{#each Array.from({ length: count }, (_, i) => i) as item}
|
||||
<li class:bx--tabs--scrollable__nav-item="{true}">
|
||||
<div class:bx--tabs__nav-link="{true}">
|
||||
<span></span>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
|
|
@ -4,6 +4,13 @@ export { Breadcrumb, BreadcrumbItem, BreadcrumbSkeleton } from "./Breadcrumb";
|
|||
export { Button, ButtonSkeleton, ButtonSet } from "./Button";
|
||||
export { Checkbox, CheckboxSkeleton } from "./Checkbox";
|
||||
export { ContentSwitcher, Switch } from "./ContentSwitcher";
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuDivider,
|
||||
ContextMenuGroup,
|
||||
ContextMenuOption,
|
||||
ContextMenuRadioGroup,
|
||||
} from "./ContextMenu";
|
||||
export { Copy } from "./Copy";
|
||||
export { CopyButton } from "./CopyButton";
|
||||
export { ComboBox } from "./ComboBox";
|
||||
|
@ -77,6 +84,7 @@ export { OrderedList } from "./OrderedList";
|
|||
export { OverflowMenu, OverflowMenuItem } from "./OverflowMenu";
|
||||
export { Pagination, PaginationSkeleton } from "./Pagination";
|
||||
export { PaginationNav } from "./PaginationNav";
|
||||
export { Popover } from "./Popover";
|
||||
export {
|
||||
ProgressIndicator,
|
||||
ProgressIndicatorSkeleton,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue