Alignment with Carbon version 10.32 (#588)

* feat(code-snippet): add copy functionality

- docs: add custom feedback copy text example

* feat(tile): support disabled state for SelectableTile, RadioTile

Closes #539

* build(rollup): add clipboard-copy to globals

* feat(copy-button): add copy functionality

* feat(content-switcher): deprecate the light prop

- docs: remove the light variant example

* fix(toolbar-search): remove outer div

* feat(search): add searchClass prop

* fix(composed-modal): set hasScrollingContent class on ModalBody

* docs(data-table): add expandable size examples

* feat(tooltip): add TooltipFooter component

* fix(time-picker): correctly display invalidText

* feat(breadcrumb): support overflow menu

* feat(multi-select): export inputRef prop

* chore(deps-dev): upgrade carbon-components to v10.32.0

* feat(form): add noMargin prop to FormGroup

* docs(tooltip): document TooltipFooter

* feat(context-menu): support danger kind for ContextMenuOption

* feat(data-table): support rendering empty table header in skeleton

* refactor(types): use shorter import path in DataTableSkeleton

* feat(data-table): allow sorting to be disabled for a specific header

* docs(data-table): update example to desort the Protocol header

As an example, it makes more sense because all the values ("http") are the same.

* fix(context-menu): set initial y offset of context menu based on window height #577

* fix(context-menu): render submenu based on viewport constraints #577
This commit is contained in:
Eric Liu 2021-04-02 13:31:53 -07:00 committed by GitHub
commit fa9b90cd79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 825 additions and 233 deletions

View file

@ -13,6 +13,10 @@
export let isCurrentPage = false;
import Link from "../Link/Link.svelte";
import { setContext } from "svelte";
setContext("BreadcrumbItem", {});
</script>
<li

View file

@ -8,6 +8,7 @@
/**
* Set the code snippet text
* Alternatively, use the default slot (e.g., <CodeSnippet>{`code`}</CodeSnippet>)
* You must use the `code` prop to copy the code
* @type {string}
*/
export let code = undefined;
@ -75,18 +76,26 @@
/** Obtain a reference to the pre HTML element */
export let ref = null;
import { tick } from "svelte";
import { createEventDispatcher, tick } from "svelte";
import copy from "clipboard-copy";
import ChevronDown16 from "carbon-icons-svelte/lib/ChevronDown16/ChevronDown16.svelte";
import Button from "../Button/Button.svelte";
import Copy from "../Copy/Copy.svelte";
import CopyButton from "../CopyButton/CopyButton.svelte";
import CodeSnippetSkeleton from "./CodeSnippetSkeleton.svelte";
const dispatch = createEventDispatcher();
function setShowMoreLess() {
const { height } = ref.getBoundingClientRect();
if (height > 0) showMoreLess = ref.getBoundingClientRect().height > 255;
}
function copyCode() {
copy(code);
dispatch("copy");
}
$: expandText = expanded ? showLessText : showMoreText;
$: if (type === "multi" && ref) {
if (code === undefined) setShowMoreLess();
@ -138,6 +147,7 @@
{wrapText && 'bx--snippet--wraptext'}"
{...$$restProps}
on:click
on:click="{copyCode}"
on:mouseover
on:mouseenter
on:mouseleave
@ -182,6 +192,7 @@
feedbackTimeout="{feedbackTimeout}"
iconDescription="{copyButtonDescription}"
on:click
on:click="{copyCode}"
on:animationend
/>
{/if}

View file

@ -11,6 +11,7 @@
role="{hasScrollingContent ? 'region' : undefined}"
class:bx--modal-content="{true}"
class:bx--modal-content--with-form="{hasForm}"
class:bx--modal-scroll-content="{hasScrollingContent}"
{...$$restProps}
>
<slot />

View file

@ -6,7 +6,10 @@
/** Set the selected index of the switch item */
export let selectedIndex = 0;
/** Set to `true` to enable the light variant */
/**
* Set to `true` to enable the light variant
* @deprecated
*/
export let light = false;
/**

View file

@ -26,6 +26,7 @@
const position = writable([x, y]);
const currentIndex = writable(-1);
const hasPopup = writable(false);
const menuOffsetX = writable(0);
const ctx = getContext("ContextMenu");
let options = [];
@ -44,6 +45,7 @@
}
setContext("ContextMenu", {
menuOffsetX,
currentIndex,
position,
close,
@ -77,8 +79,26 @@
<svelte:window
on:contextmenu|preventDefault="{(e) => {
if (level > 1) return;
if (open || x === 0) x = e.x;
if (open || y === 0) y = e.y;
const { height, width } = ref.getBoundingClientRect();
if (open || x === 0) {
if (window.innerWidth - width < e.x) {
x = e.x - width;
} else {
x = e.x;
}
}
if (open || y === 0) {
menuOffsetX.set(e.x);
if (window.innerHeight - height < e.y) {
y = e.y - height;
} else {
y = e.y;
}
}
position.set([x, y]);
open = true;
}}"

View file

@ -1,4 +1,10 @@
<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;
@ -64,11 +70,16 @@
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;
@ -106,6 +117,7 @@
return () => {
unsubPosition();
unsubMenuOffsetX();
if (unsubCurrentIds) unsubCurrentIds();
if (unsubCurrentId) unsubCurrentId();
if (typeof timeoutHover === "number") clearTimeout(timeoutHover);
@ -118,7 +130,13 @@
$: ctx.setPopup(submenuOpen);
$: if (submenuOpen) {
const { width, y } = ref.getBoundingClientRect();
submenuPosition = [rootMenuPosition[0] + width, y];
let x = rootMenuPosition[0] + width;
if (window.innerWidth - menuOffsetX < width) {
x = rootMenuPosition[0] - width;
}
submenuPosition = [x, y];
}
$: {
if (isSelectable) {
@ -158,6 +176,7 @@
class:bx--context-menu-option="{true}"
class:bx--context-menu-option--disabled="{true}"
class:bx--context-menu-option--active="{subOptions && submenuOpen}"
class:bx--context-menu-option--danger="{!subOptions && kind === 'danger'}"
indented="{indented}"
aria-checked="{isSelectable || isRadio ? selected : undefined}"
data-nested="{ref &&

View file

@ -4,8 +4,18 @@
/** Set the title and ARIA label for the copy button */
export let iconDescription = "Copy to clipboard";
import { Copy } from "../Copy";
import Copy16 from "carbon-icons-svelte/lib/Copy16";
/**
* Specify the text to copy
* @type {string}
*/
export let text = undefined;
import Copy from "../Copy/Copy.svelte";
import Copy16 from "carbon-icons-svelte/lib/Copy16/Copy16.svelte";
import copy from "clipboard-copy";
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
</script>
<Copy
@ -14,6 +24,12 @@
title="{iconDescription}"
{...$$restProps}
on:click
on:click="{() => {
if (text !== undefined) {
copy(text);
dispatch('copy');
}
}}"
on:animationend
>
<Copy16 class="bx--snippet__icon" />

View file

@ -2,8 +2,8 @@
/**
* @typedef {string} DataTableKey
* @typedef {any} DataTableValue
* @typedef {{ key: DataTableKey; empty: boolean; display?: (item: Value) => DataTableValue; sort?: (a: DataTableValue, b: DataTableValue) => (0 | -1 | 1); columnMenu?: boolean; }} DataTableEmptyHeader
* @typedef {{ key: DataTableKey; value: DataTableValue; display?: (item: Value) => DataTableValue; sort?: (a: DataTableValue, b: DataTableValue) => (0 | -1 | 1); columnMenu?: boolean; }} DataTableNonEmptyHeader
* @typedef {{ key: DataTableKey; empty: boolean; display?: (item: Value) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => (0 | -1 | 1)); columnMenu?: boolean; }} DataTableEmptyHeader
* @typedef {{ key: DataTableKey; value: DataTableValue; display?: (item: Value) => DataTableValue; sort?: false | ((a: DataTableValue, b: DataTableValue) => (0 | -1 | 1)); columnMenu?: boolean; }} DataTableNonEmptyHeader
* @typedef {DataTableNonEmptyHeader | DataTableEmptyHeader} DataTableHeader
* @typedef {{ id: any; [key: string]: DataTableValue; }} DataTableRow
* @typedef {string} DataTableRowId
@ -13,7 +13,7 @@
* @slot {{ row: DataTableRow; cell: DataTableCell; }} cell
* @event {{ header?: DataTableHeader; row?: DataTableRow; cell?: DataTableCell; }} click
* @event {{ expanded: boolean; }} click:header--expand
* @event {{ header: DataTableHeader; sortDirection: "ascending" | "descending" | "none" }} click:header
* @event {{ header: DataTableHeader; sortDirection?: "ascending" | "descending" | "none" }} click:header
* @event {DataTableRow} click:row
* @event {DataTableRow} mouseenter:row
* @event {DataTableRow} mouseleave:row
@ -250,20 +250,26 @@
<th scope="col"></th>
{:else}
<TableHeader
disableSorting="{header.sort === false}"
on:click="{() => {
dispatch('click', { header });
let active = header.key === $sortHeader.key;
let currentSortDirection = active
? $sortHeader.sortDirection
: 'none';
let sortDirection = sortDirectionMap[currentSortDirection];
dispatch('click:header', { header, sortDirection });
sortHeader.set({
id: sortDirection === 'none' ? null : $thKeys[header.key],
key: header.key,
sort: header.sort,
sortDirection,
});
if (header.sort === false) {
dispatch('click:header', { header });
} else {
let active = header.key === $sortHeader.key;
let currentSortDirection = active
? $sortHeader.sortDirection
: 'none';
let sortDirection = sortDirectionMap[currentSortDirection];
dispatch('click:header', { header, sortDirection });
sortHeader.set({
id: sortDirection === 'none' ? null : $thKeys[header.key],
key: header.key,
sort: header.sort,
sortDirection,
});
}
}}"
>
<slot name="cell-header" header="{header}">{header.value}</slot>

View file

@ -1,5 +1,5 @@
<script>
/** @extends {"../DataTable/DataTable"} DataTableHeader */
/** @extends {"./DataTable"} DataTableHeader */
/**
* Specify the number of columns
@ -77,7 +77,11 @@
<thead>
<tr>
{#each cols as col (col)}
<th>{values[col] || ""}</th>
{#if typeof values[col] === "object" && values[col].empty === true}
<th></th>
{:else}
<th>{values[col] || ""}</th>
{/if}
{/each}
</tr>
</thead>

View file

@ -1,4 +1,7 @@
<script>
/** Set to `true` to disable sorting on this specific cell */
export let disableSorting = false;
/** Specify the `scope` attribute */
export let scope = "col";
@ -12,8 +15,8 @@
export let id = "ccs-" + Math.random().toString(36);
import { getContext } from "svelte";
import ArrowUp20 from "carbon-icons-svelte/lib/ArrowUp20";
import ArrowsVertical20 from "carbon-icons-svelte/lib/ArrowsVertical20";
import ArrowUp20 from "carbon-icons-svelte/lib/ArrowUp20/ArrowUp20.svelte";
import ArrowsVertical20 from "carbon-icons-svelte/lib/ArrowsVertical20/ArrowsVertical20.svelte";
const { sortHeader, tableSortable, add } = getContext("DataTable");
@ -24,7 +27,7 @@
$: ariaLabel = translateWithId();
</script>
{#if $tableSortable}
{#if $tableSortable && !disableSorting}
<th
aria-sort="{active ? $sortHeader.sortDirection : 'none'}"
scope="{scope}"

View file

@ -32,32 +32,33 @@
await tick();
ref.focus();
}
$: classes = [
expanded && "bx--toolbar-search-container-active",
persistent
? "bx--toolbar-search-container-persistent"
: "bx--toolbar-search-container-expandable",
disabled && "bx--toolbar-search-container-disabled",
]
.filter(Boolean)
.join(" ");
</script>
<div
tabindex="{expanded || disabled ? '-1' : tabindex}"
class:bx--toolbar-action="{true}"
class:bx--toolbar-search-container-active="{expanded}"
class:bx--toolbar-search-container-expandable="{!persistent}"
class:bx--toolbar-search-container-persistent="{persistent}"
class:bx--toolbar-search-container-disabled="{disabled}"
on:click="{expandSearch}"
<Search
size="sm"
tabindex="{tabindex}"
disabled="{disabled}"
{...$$restProps}
searchClass="{classes} {$$restProps.class}"
bind:ref
bind:value
on:clear
on:change
on:input
on:focus
on:focus="{expandSearch}"
>
<Search
size="sm"
tabindex="{expanded ? tabindex : '-1'}"
disabled="{disabled}"
{...$$restProps}
bind:ref
bind:value
on:clear
on:change
on:input
on:focus
on:blur
on:blur="{() => {
expanded = !persistent && !!value.length;
}}"
/>
</div>
on:blur
on:blur="{() => {
expanded = !persistent && !!value.length;
}}"
/>

View file

@ -5,6 +5,9 @@
/** Set to `true` to render a form requirement */
export let message = false;
/** Set to `true` for to remove the bottom margin */
export let noMargin = false;
/** Specify the message text */
export let messageText = "";
@ -15,6 +18,7 @@
<fieldset
data-invalid="{invalid || undefined}"
class:bx--fieldset="{true}"
class:bx--fieldset--no-margin="{noMargin}"
{...$$restProps}
on:click
on:mouseover

View file

@ -125,6 +125,9 @@
*/
export let name = undefined;
/** Obtain a reference to the input HTML element */
export let inputRef = null;
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";
@ -143,7 +146,7 @@
let multiSelectRef = null;
let fieldRef = null;
let selectionRef = null;
let inputRef = null;
let inputValue = "";
let initialSorted = false;
let highlightedIndex = -1;

View file

@ -54,11 +54,19 @@
/** Obtain a reference to the overflow menu element */
export let menuRef = null;
import { createEventDispatcher, setContext, afterUpdate } from "svelte";
import {
createEventDispatcher,
getContext,
setContext,
afterUpdate,
} from "svelte";
import { writable } from "svelte/store";
import OverflowMenuVertical16 from "carbon-icons-svelte/lib/OverflowMenuVertical16/OverflowMenuVertical16.svelte";
import OverflowMenuHorizontal16 from "carbon-icons-svelte/lib/OverflowMenuHorizontal16/OverflowMenuHorizontal16.svelte";
import { formatStyle } from "./formatStyle";
const ctxBreadcrumbItem = getContext("BreadcrumbItem");
const dispatch = createEventDispatcher();
const items = writable([]);
const currentId = writable(undefined);
@ -68,6 +76,10 @@
let buttonWidth = undefined;
let onMountAfterUpdate = true;
$: if (ctxBreadcrumbItem) {
icon = OverflowMenuHorizontal16;
}
setContext("OverflowMenu", {
focusedId,
add: ({ id, text, primaryFocus }) => {
@ -123,6 +135,11 @@
} else if (direction === "bottom") {
menuRef.style.top = height + "px";
}
if (ctxBreadcrumbItem) {
menuRef.style.top = height + 10 + "px";
menuRef.style.left = -11 + "px";
}
}
if (!open) {
@ -218,6 +235,7 @@
class:bx--overflow-menu-options--light="{light}"
class:bx--overflow-menu-options--sm="{size === 'sm'}"
class:bx--overflow-menu-options--xl="{size === 'xl'}"
class:bx--breadcrumb-menu-options="{!!ctxBreadcrumbItem}"
class:menuOptionsClass
>
<slot />

View file

@ -12,6 +12,9 @@
*/
export let size = "xl";
/** Specify the class name passed to the outer div element */
export let searchClass = "";
/**
* Set to `true` to display the skeleton state
* @type {boolean} [skeleton=false]
@ -98,6 +101,7 @@
class:bx--search--sm="{size === 'sm' || small}"
class:bx--search--lg="{size === 'lg'}"
class:bx--search--xl="{size === 'xl'}"
class="{searchClass}"
>
<Search16 class="bx--search-magnifier" />
<label id="{id}-search" for="{id}" class:bx--label="{true}"

View file

@ -5,6 +5,9 @@
/** Set to `true` to enable the light variant */
export let light = false;
/** Set to `true` to disable the tile */
export let disabled = false;
/** Specify the value of the radio input */
export let value = "";
@ -21,7 +24,7 @@
export let name = "";
import { getContext } from "svelte";
import CheckmarkFilled16 from "carbon-icons-svelte/lib/CheckmarkFilled16";
import CheckmarkFilled16 from "carbon-icons-svelte/lib/CheckmarkFilled16/CheckmarkFilled16.svelte";
const { add, update, selectedValue } = getContext("TileGroup");
@ -36,14 +39,17 @@
name="{name}"
value="{value}"
checked="{checked}"
tabindex="{tabindex}"
tabindex="{disabled ? undefined : tabindex}"
disabled="{disabled}"
class:bx--tile-input="{true}"
on:change
on:change="{() => {
if (disabled) return;
update(value);
}}"
on:keydown
on:keydown="{(e) => {
if (disabled) return;
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
update(value);
@ -56,6 +62,7 @@
class:bx--tile--selectable="{true}"
class:bx--tile--is-selected="{checked}"
class:bx--tile--light="{light}"
class:bx--tile--disabled="{disabled}"
{...$$restProps}
on:click
on:mouseover

View file

@ -5,6 +5,9 @@
/** Set to `true` to enable the light variant */
export let light = false;
/** Set to `true` to disable the tile */
export let disabled = false;
/** Specify the title of the selectable tile */
export let title = "title";
@ -30,11 +33,11 @@
export let ref = null;
import { createEventDispatcher } from "svelte";
import CheckmarkFilled16 from "carbon-icons-svelte/lib/CheckmarkFilled16";
import CheckmarkFilled16 from "carbon-icons-svelte/lib/CheckmarkFilled16/CheckmarkFilled16.svelte";
const dispatch = createEventDispatcher();
$: dispatch(selected ? "select" : "deselect", id);
$: if (!disabled) dispatch(selected ? "select" : "deselect", id);
</script>
<input
@ -47,17 +50,20 @@
value="{value}"
name="{name}"
title="{title}"
disabled="{disabled}"
/>
<label
for="{id}"
tabindex="{tabindex}"
tabindex="{disabled ? undefined : tabindex}"
class:bx--tile="{true}"
class:bx--tile--selectable="{true}"
class:bx--tile--is-selected="{selected}"
class:bx--tile--light="{light}"
class:bx--tile--disabled="{disabled}"
{...$$restProps}
on:click
on:click|preventDefault="{() => {
if (disabled) return;
selected = !selected;
}}"
on:mouseover
@ -65,6 +71,7 @@
on:mouseleave
on:keydown
on:keydown="{(e) => {
if (disabled) return;
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
selected = !selected;

View file

@ -67,6 +67,7 @@
<div
class:bx--time-picker="{true}"
class:bx--time-picker--light="{light}"
class:bx--time-picker--invalid="{invalid}"
class:bx--time-picker--sm="{size === 'sm'}"
class:bx--time-picker--xl="{size === 'xl'}"
class:bx--select--light="{light}"

View file

@ -63,10 +63,14 @@
/** Obtain a reference to the icon HTML element */
export let refIcon = null;
import { createEventDispatcher, afterUpdate } from "svelte";
import { createEventDispatcher, afterUpdate, setContext } from "svelte";
import { writable } from "svelte/store";
import Information16 from "carbon-icons-svelte/lib/Information16/Information16.svelte";
const dispatch = createEventDispatcher();
const tooltipOpen = writable(open);
setContext("Tooltip", { tooltipOpen });
function onKeydown(e) {
if (e.key === "Escape") {
@ -143,6 +147,7 @@
}
});
$: tooltipOpen.set(open);
$: dispatch(open ? "open" : "close");
$: buttonProps = {
role: "button",

View file

@ -0,0 +1,29 @@
<script>
/** Specify a selector to be focused inside the footer when opening the tooltip */
export let selectorPrimaryFocus = "a[href], button:not([disabled])";
import { getContext, onMount } from "svelte";
let ref = null;
let open = false;
const ctx = getContext("Tooltip");
const unsubscribe = ctx.tooltipOpen.subscribe((tooltipOpen) => {
open = tooltipOpen;
});
onMount(() => {
return () => {
unsubscribe();
};
});
$: if (open && ref) {
const node = ref.querySelector(selectorPrimaryFocus);
if (node) node.focus();
}
</script>
<div bind:this="{ref}" class:bx--tooltip__footer="{true}">
<slot />
</div>

View file

@ -1 +1,2 @@
export { default as Tooltip } from "./Tooltip.svelte";
export { default as TooltipFooter } from "./TooltipFooter.svelte";

View file

@ -121,7 +121,7 @@ export {
export { TimePicker, TimePickerSelect } from "./TimePicker";
export { Toggle, ToggleSkeleton } from "./Toggle";
export { ToggleSmall, ToggleSmallSkeleton } from "./ToggleSmall";
export { Tooltip } from "./Tooltip";
export { Tooltip, TooltipFooter } from "./Tooltip";
export { TooltipDefinition } from "./TooltipDefinition";
export { TooltipIcon } from "./TooltipIcon";
export { Truncate } from "./Truncate";