diff --git a/src/components/SplitButton/SplitButton.vue b/src/components/SplitButton/SplitButton.vue
new file mode 100644
index 00000000..0377e11c
--- /dev/null
+++ b/src/components/SplitButton/SplitButton.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/SplitButton/__tests__/__snapshots__/split-button.test.js.snap b/src/components/SplitButton/__tests__/__snapshots__/split-button.test.js.snap
new file mode 100644
index 00000000..e97e9284
--- /dev/null
+++ b/src/components/SplitButton/__tests__/__snapshots__/split-button.test.js.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Split Button renders correctly 1`] = `
+
+
+
+
+
+`;
+
+exports[`Split Button when disabled renders correctly 1`] = `
+
+
+
+
+
+`;
diff --git a/src/components/SplitButton/__tests__/split-button.test.js b/src/components/SplitButton/__tests__/split-button.test.js
new file mode 100644
index 00000000..06107b71
--- /dev/null
+++ b/src/components/SplitButton/__tests__/split-button.test.js
@@ -0,0 +1,74 @@
+import { mount, createLocalVue } from "@vue/test-utils";
+import FundamentalVue from "./../../../";
+import FdSplitButton from "./../SplitButton.vue";
+import FdSplitButtonAction from "./../../SplitButtonAction/SplitButtonAction.vue";
+import FdSplitButtonAuxiliary from "./../../SplitButtonAuxiliary/SplitButtonAuxiliary.vue";
+
+const createSplitButton = ({ propsData } = { propsData: {} }) => {
+ const localVue = createLocalVue();
+ localVue.use(FundamentalVue);
+ return mount(FdSplitButton, {
+ localVue,
+ propsData,
+ slots: {
+ default: "Hello World"
+ }
+ });
+};
+
+describe("Split Button", () => {
+ test("renders correctly", () => {
+ expect(createSplitButton().element).toMatchSnapshot();
+ });
+
+ test("emits click-event when action-button is clicked", () => {
+ const wrapper = createSplitButton();
+ const actionButtonWrapper = wrapper.find(FdSplitButtonAction);
+ expect(actionButtonWrapper.exists()).toBe(true);
+ actionButtonWrapper.trigger("click");
+ expect(wrapper.emitted("click")).toHaveLength(1);
+ });
+
+ test("emits click:auxiliary-event when auxiliary-button is clicked", () => {
+ const wrapper = createSplitButton();
+ const auxiliaryButtonWrapper = wrapper.find(FdSplitButtonAuxiliary);
+ expect(auxiliaryButtonWrapper.exists()).toBe(true);
+ auxiliaryButtonWrapper.trigger("click");
+ expect(wrapper.emitted("click:auxiliary")).toHaveLength(1);
+ expect(wrapper.emitted("click")).toBeUndefined();
+ });
+
+ describe("when disabled", () => {
+ /** @type {import("@vue/test-utils").Wrapper} */
+ let disabledWrapper;
+ beforeEach(() => {
+ disabledWrapper = createSplitButton({
+ propsData: {
+ state: "disabled"
+ }
+ });
+ });
+
+ test("renders correctly", () => {
+ expect(disabledWrapper.element).toMatchSnapshot();
+ });
+
+ test("click:auxiliary-event is not emitted when auxiliary-button is clicked", () => {
+ const auxiliaryButtonWrapper = disabledWrapper.find(
+ FdSplitButtonAuxiliary
+ );
+ expect(auxiliaryButtonWrapper.exists()).toBe(true);
+ auxiliaryButtonWrapper.trigger("click");
+ expect(disabledWrapper.emitted("click:auxiliary")).toBeUndefined();
+ expect(disabledWrapper.emitted("click")).toBeUndefined();
+ });
+
+ test("click-event is not emitted when action-button is clicked", () => {
+ const actionButtonWrapper = disabledWrapper.find(FdSplitButtonAction);
+ expect(actionButtonWrapper.exists()).toBe(true);
+ actionButtonWrapper.trigger("click");
+ expect(disabledWrapper.emitted("click:auxiliary")).toBeUndefined();
+ expect(disabledWrapper.emitted("click")).toBeUndefined();
+ });
+ });
+});
diff --git a/src/components/SplitButton/index.js b/src/components/SplitButton/index.js
new file mode 100644
index 00000000..2bdc50b3
--- /dev/null
+++ b/src/components/SplitButton/index.js
@@ -0,0 +1,4 @@
+import SplitButton from "./SplitButton.vue";
+import { pluginify } from "./../../util";
+export default pluginify(SplitButton);
+export { SplitButton };
diff --git a/src/components/SplitButtonAction/SplitButtonAction.vue b/src/components/SplitButtonAction/SplitButtonAction.vue
new file mode 100644
index 00000000..6b70cc43
--- /dev/null
+++ b/src/components/SplitButtonAction/SplitButtonAction.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
diff --git a/src/components/SplitButtonAction/index.js b/src/components/SplitButtonAction/index.js
new file mode 100644
index 00000000..a16a3c99
--- /dev/null
+++ b/src/components/SplitButtonAction/index.js
@@ -0,0 +1,4 @@
+import SplitButtonAction from "./SplitButtonAction.vue";
+import { pluginify } from "./../../util";
+export default pluginify(SplitButtonAction);
+export { SplitButtonAction };
diff --git a/src/components/SplitButtonAuxiliary/SplitButtonAuxiliary.vue b/src/components/SplitButtonAuxiliary/SplitButtonAuxiliary.vue
new file mode 100644
index 00000000..b6fd0b2c
--- /dev/null
+++ b/src/components/SplitButtonAuxiliary/SplitButtonAuxiliary.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
diff --git a/src/components/SplitButtonAuxiliary/index.js b/src/components/SplitButtonAuxiliary/index.js
new file mode 100644
index 00000000..c30d3a72
--- /dev/null
+++ b/src/components/SplitButtonAuxiliary/index.js
@@ -0,0 +1,4 @@
+import SplitButtonAuxiliary from "./SplitButtonAuxiliary.vue";
+import { pluginify } from "./../../util";
+export default pluginify(SplitButtonAuxiliary);
+export { SplitButtonAuxiliary };
diff --git a/src/components/index.js b/src/components/index.js
index e5229d14..8b0d5fb7 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -31,6 +31,9 @@ import SearchInput from "./SearchInput";
import SideNav from "./SideNav";
import Spinner from "./Spinner";
import Status from "./Status";
+import SplitButton from "./SplitButton";
+import SplitButtonAuxiliary from "./SplitButtonAuxiliary";
+import SplitButtonAction from "./SplitButtonAction";
import Table from "./Table";
import Tabs from "./Tabs";
import Tile from "./Tile";
@@ -75,6 +78,9 @@ const plugin = {
SideNav,
Spinner,
Status,
+ SplitButton,
+ SplitButtonAuxiliary,
+ SplitButtonAction,
Table,
Tabs,
Tile,
diff --git a/src/docs/pages/Button/2.1-split-button.vue b/src/docs/pages/Button/2.1-split-button.vue
new file mode 100644
index 00000000..9092916a
--- /dev/null
+++ b/src/docs/pages/Button/2.1-split-button.vue
@@ -0,0 +1,104 @@
+Split Button Playground
+
+`fd-split-button` is a composition of two other components:
+
+1. `fd-split-button-action`: The *main* area on the left. `fd-split-button` will emit a `click`-event when the action-area is clicked.
+2. `fd-split-button-auxiliary`: The area on the right. `fd-split-button` will emit a `click:auxiliary`-event when the auxiliary-area is clicked. You should show a popover/menu in when this event is emitted.
+
+
+
+`fd-split-button` does not display a menu or popover when clicked. It is up to you to set that up.
+
+
+
+
+ {{ text }}
+
+
+
+
+
+ Styling
+
+
+
+
+
+
+
+ Type
+
+
+
+
+
+
+
+
+
+ Icon
+
+
+
+
+
+
+
+ State
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/docs/pages/Button/2.2-split-button-popover.vue b/src/docs/pages/Button/2.2-split-button-popover.vue
new file mode 100644
index 00000000..da704c61
--- /dev/null
+++ b/src/docs/pages/Button/2.2-split-button-popover.vue
@@ -0,0 +1,29 @@
+Split Button With Menu
+
+`fd-split-button` works perfectly together with `fd-menu-popover`.
+
+
+
+
+
+ Checkout
+
+
+ Option 1
+ Option 2
+ Option 3
+ Option 4
+
+
+
+
+
+
diff --git a/src/tools/index.js b/src/tools/index.js
index 99fd8030..c6ba9b23 100644
--- a/src/tools/index.js
+++ b/src/tools/index.js
@@ -1,3 +1,4 @@
+// @ts-check
const { parser } = require("@vuese/parser");
const { Render } = require("@vuese/markdown-render");
const klawSync = require("klaw-sync");
@@ -6,6 +7,97 @@ const fs = require("fs");
const isSFCFile = item => Path.parse(item.path).ext === ".vue";
+/**
+ * For context see below.
+ * @param {import("@vuese/parser").SlotResult} lhsSlot
+ * @param {import("@vuese/parser").SlotResult} rhsSlot
+ * @returns {import("@vuese/parser").SlotResult}
+ */
+const mergedSlot = (lhsSlot, rhsSlot) => {
+ const name = lhsSlot.name;
+ const {
+ describe: lhsDescribe,
+ backerDesc: lhsBackerDesc,
+ bindings: lhsBindings,
+ scoped: lhsScoped
+ } = lhsSlot;
+ const {
+ describe: rhsDescribe,
+ backerDesc: rhsBackerDesc,
+ bindings: rhsBindings,
+ scoped: rhsScoped
+ } = rhsSlot;
+ const describe =
+ lhsDescribe.length >= rhsDescribe.length ? lhsDescribe : rhsDescribe;
+ const backerDesc =
+ lhsBackerDesc.length >= rhsBackerDesc.length
+ ? lhsBackerDesc
+ : rhsBackerDesc;
+ const bindings =
+ Object.keys(lhsBindings).length >= Object.keys(rhsBindings).length
+ ? lhsBindings
+ : rhsBindings;
+ // The default seems to be 'false' – so if lhsScoped is true we take that. Otherwise we simply use rhsScoped
+ const scoped = lhsScoped === true ? true : rhsScoped;
+ return { scoped, name, describe, backerDesc, bindings };
+};
+
+/**
+ * For context see below.
+ * @param {import("@vuese/parser").SlotResult} slot
+ * @param {import("@vuese/parser").SlotResult[]} otherSlots
+ * @returns {import("@vuese/parser").SlotResult}
+ */
+const fixedSlot = (slot, otherSlots) => {
+ const duplicateSlots = otherSlots.filter(({ name }) => name === slot.name);
+ if (duplicateSlots.length === 0) {
+ return slot; // no duplicates – phew
+ }
+ let fixed = slot;
+ duplicateSlots.forEach(otherSlot => {
+ fixed = mergedSlot(fixed, otherSlot);
+ });
+ return fixed;
+};
+
+/**
+ * For context see fixedParserResult below.
+ * @param {import("@vuese/parser").SlotResult[]} slots
+ * @returns {import("@vuese/parser").SlotResult[]}
+ */
+const fixedParserSlots = slots => {
+ const fixedButDuplicated = slots.map((slot, index) => {
+ const otherSlots = [...slots];
+ // remove the current slot from the copy
+ // the result are the 'other slots'
+ otherSlots.splice(index, 1);
+ return fixedSlot(slot, otherSlots);
+ });
+ // fixedButDuplicated now contains correct slots but duplicated still remain.
+ const result = [];
+ fixedButDuplicated.forEach(slot => {
+ const alreadyInResult =
+ result.findIndex(resultSlot => resultSlot.name === slot.name) >= 0;
+ if (alreadyInResult) {
+ return;
+ }
+ result.push(slot);
+ });
+ return result;
+};
+
+/**
+ * Fixes the parser result. There is currently a bug in @vuese/parser
+ * which causes result.slots to contain duplicate entries.
+ * See: https://github.com/vuese/vuese/issues/83
+ * @param {import("@vuese/parser").ParserResult} result
+ * @returns {import("@vuese/parser").ParserResult}
+ */
+const fixedParserResult = result => {
+ const { slots = [] } = result;
+ return { ...result, slots: fixedParserSlots(slots) };
+};
+
const paths = klawSync("./src/components", {
traverseAll: true,
nodir: true,
@@ -15,8 +107,7 @@ const paths = klawSync("./src/components", {
const parseFileAtPath = path => {
const source = fs.readFileSync(path, "utf-8");
try {
- const parserRes = parser(source);
- return parserRes;
+ return fixedParserResult(parser(source));
} catch (e) {
return {};
}