diff --git a/src-docs/src/views/list_group/list_group_example.js b/src-docs/src/views/list_group/list_group_example.js
index d4642cb9066..bf2aa4cd593 100644
--- a/src-docs/src/views/list_group/list_group_example.js
+++ b/src-docs/src/views/list_group/list_group_example.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { Link } from 'react-router';
import { renderToHtml } from '../../services';
@@ -7,8 +8,10 @@ import { GuideSectionTypes } from '../../components';
import {
EuiListGroup,
EuiListGroupItem,
+ EuiPinnableListGroup,
EuiCode,
} from '../../../../src/components';
+import { EuiPinnableListGroupItem } from './props';
import ListGroup from './list_group';
const listGroupSource = require('!!raw-loader!./list_group');
@@ -30,6 +33,10 @@ import ListGroupItemColor from './list_group_item_color';
const listGroupItemColorSource = require('!!raw-loader!./list_group_item_color');
const listGroupItemColorHtml = renderToHtml(ListGroupItemColor);
+import PinnableListGroup from './pinnable_list_group';
+const pinnableListGroupSource = require('!!raw-loader!./pinnable_list_group');
+const pinnableListGroupHtml = renderToHtml(PinnableListGroup);
+
export const ListGroupExample = {
title: 'List group',
sections: [
@@ -205,6 +212,58 @@ export const ListGroupExample = {
label="Primary"
color="primary"
size="s"
+/>`,
+ },
+ {
+ title: 'Pinnable list group',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: pinnableListGroupSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: pinnableListGroupHtml,
+ },
+ ],
+ text: (
+ <>
+
+ EuiPinnableListGroup is simply an extra wrapper
+ around an{' '}
+
+ EuiListGroup
+ {' '}
+ that provides visual indicators for pinning .
+
+
+ Pinning is the concept that users can click a pin icon and add it to
+ a subset of links (most likely shown in different list group). By
+ providing an onPinClick handler, the component
+ will automatically add the pin action to the item. However, the
+ consuming application must manage the listItem s
+ and their pinned state.
+
+
+ In order to get the full benefit of using{' '}
+ EuiPinnableListGroup , the component only supports
+ providing list items via the listItem prop and
+ does not support children .
+
+ >
+ ),
+ props: { EuiPinnableListGroup, EuiPinnableListGroupItem },
+ demo: ,
+ snippet: ` {}}
+ listItems={[
+ {
+ label: 'A link',
+ href: '#',
+ pinned: true,
+ isActive: true,
+ },
+ ]}
/>`,
},
],
diff --git a/src-docs/src/views/list_group/list_group_link_actions.js b/src-docs/src/views/list_group/list_group_link_actions.js
index e3e31b1df89..ec6edf64304 100644
--- a/src-docs/src/views/list_group/list_group_link_actions.js
+++ b/src-docs/src/views/list_group/list_group_link_actions.js
@@ -68,7 +68,7 @@ export default class extends Component {
extraAction={{
color: 'subdued',
onClick: this.link1Clicked,
- iconType: favorite1 === 'link1' ? 'pinFilled' : 'pin',
+ iconType: favorite1 === 'link1' ? 'starFilled' : 'starEmpty',
iconSize: 's',
'aria-label': 'Favorite link1',
alwaysShow: favorite1 === 'link1',
@@ -83,7 +83,7 @@ export default class extends Component {
extraAction={{
color: 'subdued',
onClick: this.link2Clicked,
- iconType: favorite2 === 'link2' ? 'pinFilled' : 'pin',
+ iconType: favorite2 === 'link2' ? 'starFilled' : 'starEmpty',
iconSize: 's',
'aria-label': 'Favorite link2',
alwaysShow: favorite2 === 'link2',
@@ -98,7 +98,7 @@ export default class extends Component {
extraAction={{
color: 'subdued',
onClick: this.link3Clicked,
- iconType: favorite3 === 'link3' ? 'pinFilled' : 'pin',
+ iconType: favorite3 === 'link3' ? 'starFilled' : 'starEmpty',
iconSize: 's',
'aria-label': 'Favorite link3',
alwaysShow: favorite3 === 'link3',
@@ -114,7 +114,7 @@ export default class extends Component {
extraAction={{
color: 'subdued',
onClick: () => window.alert('Action clicked'),
- iconType: 'pin',
+ iconType: 'starEmpty',
iconSize: 's',
'aria-label': 'Favorite link4',
}}
diff --git a/src-docs/src/views/list_group/pinnable_list_group.tsx b/src-docs/src/views/list_group/pinnable_list_group.tsx
new file mode 100644
index 00000000000..8f0ba4f7564
--- /dev/null
+++ b/src-docs/src/views/list_group/pinnable_list_group.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+
+import {
+ EuiPinnableListGroup,
+ EuiPinnableListGroupItemProps,
+} from '../../../../src/components/list_group';
+
+const someListItems: EuiPinnableListGroupItemProps[] = [
+ {
+ label: 'Label with iconType',
+ iconType: 'stop',
+ },
+ {
+ label: 'Pinned button with onClick',
+ pinned: true,
+ onClick: e => {
+ console.log('Pinned button clicked', e);
+ },
+ },
+ {
+ label: 'Link with href and custom pin titles',
+ href: '/#',
+ },
+ {
+ label: 'Active link',
+ isActive: true,
+ href: '/#',
+ },
+ {
+ label: 'Custom extra actions will override pinning ability',
+ extraAction: {
+ iconType: 'bell',
+ alwaysShow: true,
+ 'aria-label': 'bell',
+ },
+ },
+];
+
+export default () => (
+ <>
+ {
+ console.warn('Clicked: ', item);
+ }}
+ maxWidth="none"
+ pinTitle={(item: EuiPinnableListGroupItemProps) => `Pin ${item.label}`}
+ unpinTitle={(item: EuiPinnableListGroupItemProps) =>
+ `Unpin ${item.label}`
+ }
+ />
+ >
+);
diff --git a/src-docs/src/views/list_group/props.tsx b/src-docs/src/views/list_group/props.tsx
new file mode 100644
index 00000000000..c131a02172c
--- /dev/null
+++ b/src-docs/src/views/list_group/props.tsx
@@ -0,0 +1,7 @@
+import React, { FunctionComponent } from 'react';
+
+import { EuiPinnableListGroupItemProps } from '../../../../src/components/list_group';
+
+export const EuiPinnableListGroupItem: FunctionComponent<
+ EuiPinnableListGroupItemProps
+> = () =>
;
diff --git a/src/components/collapsible_nav/_index.scss b/src/components/collapsible_nav/_index.scss
index fa7acfa01df..a31c44be949 100644
--- a/src/components/collapsible_nav/_index.scss
+++ b/src/components/collapsible_nav/_index.scss
@@ -1,4 +1,4 @@
@import 'variables';
-@import 'collapsible_nav_group/index';
@import 'collapsible_nav';
+@import 'collapsible_nav_group/index';
diff --git a/src/components/index.js b/src/components/index.js
index 8846bf27027..07d30878e66 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -186,7 +186,11 @@ export {
export { EuiLink } from './link';
-export { EuiListGroup, EuiListGroupItem } from './list_group';
+export {
+ EuiListGroup,
+ EuiListGroupItem,
+ EuiPinnableListGroup,
+} from './list_group';
export {
EUI_MODAL_CANCEL_BUTTON,
diff --git a/src/components/list_group/__snapshots__/list_group.test.tsx.snap b/src/components/list_group/__snapshots__/list_group.test.tsx.snap
index c8a941b009b..860d689dbe8 100644
--- a/src/components/list_group/__snapshots__/list_group.test.tsx.snap
+++ b/src/components/list_group/__snapshots__/list_group.test.tsx.snap
@@ -44,6 +44,7 @@ exports[`EuiListGroup is rendered with listItems 1`] = `
+
+
+
+ Active link
+
+
+
@@ -123,6 +139,7 @@ exports[`EuiListGroup is rendered with listItems and color 1`] = `
+
+
+
+ Active link
+
+
+
diff --git a/src/components/list_group/_index.scss b/src/components/list_group/_index.scss
index bf49f315ffe..d8aaf029726 100644
--- a/src/components/list_group/_index.scss
+++ b/src/components/list_group/_index.scss
@@ -2,3 +2,4 @@
@import 'list_group';
@import 'list_group_item';
+@import 'pinnable_list_group/index';
diff --git a/src/components/list_group/_list_group_item.scss b/src/components/list_group/_list_group_item.scss
index ebade8154aa..8432f004754 100644
--- a/src/components/list_group/_list_group_item.scss
+++ b/src/components/list_group/_list_group_item.scss
@@ -13,19 +13,28 @@
background-color: $euiListGroupItemHoverBackground;
}
- &.euiListGroupItem-isClickable .euiListGroupItem__button:focus {
+ // Can't be grouped with above or else IE will ignore the whole group
+ &.euiListGroupItem-isClickable:focus-within {
background-color: $euiListGroupItemHoverBackground;
- text-decoration: underline;
}
&.euiListGroupItem--ghost {
- &.euiListGroupItem-isActive,
- &.euiListGroupItem-isClickable:hover,
- &.euiListGroupItem-isClickable .euiListGroupItem__button:focus {
+ &.euiListGroupItem-isClickable:hover {
+ background-color: $euiListGroupItemHoverBackgroundGhost;
+ }
+
+ // Can't be grouped with above or else IE will ignore the whole group
+ &.euiListGroupItem-isClickable:focus-within {
background-color: $euiListGroupItemHoverBackgroundGhost;
}
}
+ &.euiListGroupItem-isClickable:hover .euiListGroupItem__button,
+ .euiListGroupItem__button:hover,
+ .euiListGroupItem__button:focus {
+ text-decoration: underline;
+ }
+
// Style all disabled list items whether or not they are links or buttons
&.euiListGroupItem-isDisabled,
&.euiListGroupItem-isDisabled:hover,
@@ -33,9 +42,23 @@
&.euiListGroupItem-isDisabled .euiListGroupItem__button:hover,
&.euiListGroupItem-isDisabled .euiListGroupItem__button:focus {
color: $euiButtonColorDisabled;
- text-decoration: none;
cursor: not-allowed;
background-color: transparent;
+ text-decoration: none;
+ }
+}
+
+// IE doesn't support :focus-within
+@include internetExplorerOnly {
+ .euiListGroupItem__button:hover,
+ .euiListGroupItem__button:focus {
+ background-color: $euiListGroupItemHoverBackground;
+ border-radius: $euiBorderRadius;
+
+ .euiListGroupItem--ghost .euiListGroupItem__button:hover,
+ .euiListGroupItem--ghost .euiListGroupItem__button:focus {
+ background-color: $euiListGroupItemHoverBackgroundGhost;
+ }
}
}
diff --git a/src/components/list_group/index.ts b/src/components/list_group/index.ts
index a82a234ef34..51a48e94092 100644
--- a/src/components/list_group/index.ts
+++ b/src/components/list_group/index.ts
@@ -1,2 +1,7 @@
export { EuiListGroup, EuiListGroupProps } from './list_group';
export { EuiListGroupItem, EuiListGroupItemProps } from './list_group_item';
+export {
+ EuiPinnableListGroup,
+ EuiPinnableListGroupProps,
+ EuiPinnableListGroupItemProps,
+} from './pinnable_list_group';
diff --git a/src/components/list_group/list_group.test.tsx b/src/components/list_group/list_group.test.tsx
index 52a7002b4b9..5bc6fae9644 100644
--- a/src/components/list_group/list_group.test.tsx
+++ b/src/components/list_group/list_group.test.tsx
@@ -15,6 +15,7 @@ const someListItems: EuiListGroupItemProps[] = [
extraAction: {
iconType: 'bell',
alwaysShow: true,
+ 'aria-label': 'bell',
},
},
{
@@ -23,6 +24,11 @@ const someListItems: EuiListGroupItemProps[] = [
console.log('Visualize clicked', e);
},
},
+ {
+ label: 'Active link',
+ isActive: true,
+ href: '#',
+ },
{
label: 'Link with href',
href: '#',
diff --git a/src/components/list_group/pinnable_list_group/__snapshots__/pinnable_list_group.test.tsx.snap b/src/components/list_group/pinnable_list_group/__snapshots__/pinnable_list_group.test.tsx.snap
new file mode 100644
index 00000000000..52b5080d5ce
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/__snapshots__/pinnable_list_group.test.tsx.snap
@@ -0,0 +1,291 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiPinnableListGroup can have custom pin icon titles 1`] = `
+
+`;
+
+exports[`EuiPinnableListGroup is rendered 1`] = `
+
+`;
diff --git a/src/components/list_group/pinnable_list_group/_index.scss b/src/components/list_group/pinnable_list_group/_index.scss
new file mode 100644
index 00000000000..0bd00f0595e
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/_index.scss
@@ -0,0 +1 @@
+@import 'pinnable_list_group';
diff --git a/src/components/list_group/pinnable_list_group/_pinnable_list_group.scss b/src/components/list_group/pinnable_list_group/_pinnable_list_group.scss
new file mode 100644
index 00000000000..05f7191ad2a
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/_pinnable_list_group.scss
@@ -0,0 +1,9 @@
+.euiPinnableListGroup__itemExtraAction {
+ svg {
+ transform: rotate(45deg);
+ }
+}
+
+.euiPinnableListGroup__itemExtraAction-pinned:not(:hover):not(:focus) {
+ color: makeGraphicContrastColor($euiColorLightShade);
+}
diff --git a/src/components/list_group/pinnable_list_group/index.ts b/src/components/list_group/pinnable_list_group/index.ts
new file mode 100644
index 00000000000..0b9cf97d764
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/index.ts
@@ -0,0 +1,5 @@
+export {
+ EuiPinnableListGroup,
+ EuiPinnableListGroupProps,
+ EuiPinnableListGroupItemProps,
+} from './pinnable_list_group';
diff --git a/src/components/list_group/pinnable_list_group/pinnable_list_group.test.tsx b/src/components/list_group/pinnable_list_group/pinnable_list_group.test.tsx
new file mode 100644
index 00000000000..a658231b09b
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/pinnable_list_group.test.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../../test/required_props';
+
+import {
+ EuiPinnableListGroup,
+ EuiPinnableListGroupItemProps,
+} from './pinnable_list_group';
+
+const someListItems: EuiPinnableListGroupItemProps[] = [
+ {
+ label: 'Label with iconType',
+ iconType: 'stop',
+ },
+ {
+ label: 'Custom extra action',
+ extraAction: {
+ iconType: 'bell',
+ alwaysShow: true,
+ 'aria-label': 'bell',
+ },
+ },
+ {
+ label: 'Active link',
+ isActive: true,
+ href: '#',
+ },
+ {
+ label: 'Button with onClick',
+ pinned: true,
+ onClick: e => {
+ console.log('Visualize clicked', e);
+ },
+ },
+ {
+ label: 'Link with href',
+ href: '#',
+ },
+];
+
+describe('EuiPinnableListGroup', () => {
+ test('is rendered', () => {
+ const component = render(
+ {}}
+ />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('can have custom pin icon titles', () => {
+ const component = render(
+ {}}
+ pinTitle={(item: EuiPinnableListGroupItemProps) =>
+ `Pin ${item.label} to the top`
+ }
+ unpinTitle={(item: EuiPinnableListGroupItemProps) =>
+ `Unpin ${item.label} to the top`
+ }
+ />
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/src/components/list_group/pinnable_list_group/pinnable_list_group.tsx b/src/components/list_group/pinnable_list_group/pinnable_list_group.tsx
new file mode 100644
index 00000000000..b00accde19f
--- /dev/null
+++ b/src/components/list_group/pinnable_list_group/pinnable_list_group.tsx
@@ -0,0 +1,122 @@
+import React, { FunctionComponent } from 'react';
+import classNames from 'classnames';
+import { CommonProps } from '../../common';
+
+import { EuiI18n } from '../../i18n';
+import { EuiListGroup, EuiListGroupProps } from '../list_group';
+import { EuiListGroupItemProps } from '../list_group_item';
+
+const pinExtraAction: EuiListGroupItemProps['extraAction'] = {
+ color: 'primary',
+ iconType: 'pinFilled',
+ iconSize: 's',
+ className: 'euiPinnableListGroup__itemExtraAction',
+};
+
+const pinnedExtraAction: EuiListGroupItemProps['extraAction'] = {
+ color: 'primary',
+ iconType: 'pinFilled',
+ iconSize: 's',
+ className:
+ 'euiPinnableListGroup__itemExtraAction euiPinnableListGroup__itemExtraAction-pinned',
+ alwaysShow: true,
+};
+
+export type EuiPinnableListGroupItemProps = EuiListGroupItemProps & {
+ pinned?: boolean;
+};
+
+export interface EuiPinnableListGroupProps
+ extends CommonProps,
+ EuiListGroupProps {
+ /**
+ * Extends `EuiListGroupItemProps`, at the very least, expecting a `label`.
+ * See #EuiPinnableListGroupItem
+ */
+ listItems: EuiPinnableListGroupItemProps[];
+ /**
+ * Shows the pin icon and calls this function on click.
+ * Returns `item: EuiPinnableListGroupItemProps`
+ */
+ onPinClick: (item: EuiPinnableListGroupItemProps) => void;
+ /**
+ * The pin icon needs a title/aria-label for accessibility.
+ * It is a function that passes the item back and must return a string `(item) => string`.
+ * Default is `"Pin item"`
+ */
+ pinTitle?: (item: EuiPinnableListGroupItemProps) => string;
+ /**
+ * The unpin icon needs a title/aria-label for accessibility.
+ * It is a function that passes the item back and must return a string `(item) => string`.
+ * Default is `"Unpin item"`
+ */
+ unpinTitle?: (item: EuiPinnableListGroupItemProps) => string;
+}
+
+export const EuiPinnableListGroup: FunctionComponent<
+ EuiPinnableListGroupProps
+> = ({ className, listItems, pinTitle, unpinTitle, onPinClick, ...rest }) => {
+ const classes = classNames('euiPinnableListGroup', className);
+
+ // Alter listItems object with extra props
+ const getNewListItems = (
+ pinExtraActionLabel: string,
+ pinnedExtraActionLabel: string
+ ) =>
+ listItems.map(item => {
+ const { pinned, ...itemProps } = item;
+ // Make some declarations of props for the nav implementation
+ itemProps.className = classNames(
+ 'euiPinnableListGroup__item',
+ item.className
+ );
+ itemProps.size = item.size || 's';
+
+ // Add the pinning action unless the item has it's own extra action
+ if (onPinClick && !itemProps.extraAction) {
+ // Different displays for pinned vs unpinned
+ if (pinned) {
+ itemProps.extraAction = {
+ ...pinnedExtraAction,
+ title: unpinTitle ? unpinTitle(item) : pinnedExtraActionLabel,
+ 'aria-label': unpinTitle
+ ? unpinTitle(item)
+ : pinnedExtraActionLabel,
+ };
+ } else {
+ itemProps.extraAction = {
+ ...pinExtraAction,
+ title: pinTitle ? pinTitle(item) : pinnedExtraActionLabel,
+ 'aria-label': pinTitle ? pinTitle(item) : pinnedExtraActionLabel,
+ };
+ }
+ // Return the item on click
+ itemProps.extraAction.onClick = () => onPinClick(item);
+ }
+
+ return itemProps;
+ });
+
+ return (
+
+ {([pinExtraActionLabel, pinnedExtraActionLabel]: string[]) => {
+ const newListItems = getNewListItems(
+ pinExtraActionLabel,
+ pinnedExtraActionLabel
+ );
+ return (
+
+ );
+ }}
+
+ );
+};