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. + + + + + + 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`. + + + + 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 {}; }