Complete implementing popover system main features (#131)

* Make popover buttons open popover menus and add placeholder messages to all of them.
* Implement all directions for drawing, aligning, and edge-clamping popovers.
* Fix popovers so they are drawn outside their parent panel bounds and not clipped.
* Fix popover HTML to avoid nesting it inside a <button> element.
This commit is contained in:
Keavon Chambers
2021-05-21 12:22:30 -07:00
committed by GitHub
parent 4ed093bb4b
commit c8eea1e4b1
8 changed files with 204 additions and 66 deletions
+16
View File
@@ -21,6 +21,22 @@ button {
line-height: 1; line-height: 1;
color: #ddd; color: #ddd;
} }
// For placeholder messages (remove eventually)
.popover {
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0;
}
p {
margin-top: 8px;
}
}
</style> </style>
<script lang="ts"> <script lang="ts">
+18 -4
View File
@@ -12,13 +12,19 @@
<IconButton :size="24" title="Vertical Align Top"><AlignVerticalTop /></IconButton> <IconButton :size="24" title="Vertical Align Top"><AlignVerticalTop /></IconButton>
<IconButton :size="24" title="Vertical Align Center"><AlignVerticalCenter /></IconButton> <IconButton :size="24" title="Vertical Align Center"><AlignVerticalCenter /></IconButton>
<IconButton :size="24" title="Vertical Align Bottom"><AlignVerticalBottom /></IconButton> <IconButton :size="24" title="Vertical Align Bottom"><AlignVerticalBottom /></IconButton>
<DropdownButton /> <DropdownButton>
<h3>Align</h3>
<p>More alignment-related buttons will be here</p>
</DropdownButton>
<ItemDivider /> <ItemDivider />
<IconButton :size="24" title="Flip Horizontal"><FlipHorizontal /></IconButton> <IconButton :size="24" title="Flip Horizontal"><FlipHorizontal /></IconButton>
<IconButton :size="24" title="Flip Vertical"><FlipVertical /></IconButton> <IconButton :size="24" title="Flip Vertical"><FlipVertical /></IconButton>
<DropdownButton /> <DropdownButton>
<h3>Flip</h3>
<p>More flip-related buttons will be here</p>
</DropdownButton>
<ItemDivider /> <ItemDivider />
@@ -27,7 +33,10 @@
<IconButton :size="24" title="Boolean Subtract Back"><BooleanSubtractBack /></IconButton> <IconButton :size="24" title="Boolean Subtract Back"><BooleanSubtractBack /></IconButton>
<IconButton :size="24" title="Boolean Intersect"><BooleanIntersect /></IconButton> <IconButton :size="24" title="Boolean Intersect"><BooleanIntersect /></IconButton>
<IconButton :size="24" title="Boolean Difference"><BooleanDifference /></IconButton> <IconButton :size="24" title="Boolean Difference"><BooleanDifference /></IconButton>
<DropdownButton /> <DropdownButton>
<h3>Boolean</h3>
<p>More boolean-related buttons will be here</p>
</DropdownButton>
</div> </div>
<div class="spacer"></div> <div class="spacer"></div>
<div class="right side"> <div class="right side">
@@ -35,7 +44,10 @@
<IconButton :size="24" title="View Mode: Normal"><ViewModeNormal /></IconButton> <IconButton :size="24" title="View Mode: Normal"><ViewModeNormal /></IconButton>
<IconButton :size="24" title="View Mode: Outline"><ViewModeOutline /></IconButton> <IconButton :size="24" title="View Mode: Outline"><ViewModeOutline /></IconButton>
<IconButton :size="24" title="View Mode: Pixels"><ViewModePixels /></IconButton> <IconButton :size="24" title="View Mode: Pixels"><ViewModePixels /></IconButton>
<DropdownButton /> <DropdownButton>
<h3>Display Mode</h3>
<p>More display mode options will be here</p>
</DropdownButton>
</RadioPicker> </RadioPicker>
<ItemDivider /> <ItemDivider />
@@ -147,6 +159,7 @@ import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiv
import LayoutRow from "../layout/LayoutRow.vue"; import LayoutRow from "../layout/LayoutRow.vue";
import LayoutCol from "../layout/LayoutCol.vue"; import LayoutCol from "../layout/LayoutCol.vue";
import WorkingColors from "../widgets/WorkingColors.vue"; import WorkingColors from "../widgets/WorkingColors.vue";
import { PopoverDirection } from "../widgets/PopoverMount.vue";
import ShelfItem from "../widgets/ShelfItem.vue"; import ShelfItem from "../widgets/ShelfItem.vue";
import ItemDivider from "../widgets/ItemDivider.vue"; import ItemDivider from "../widgets/ItemDivider.vue";
import IconButton from "../widgets/IconButton.vue"; import IconButton from "../widgets/IconButton.vue";
@@ -304,6 +317,7 @@ export default defineComponent({
return { return {
viewportSvg: "", viewportSvg: "",
activeTool: "Select", activeTool: "Select",
PopoverDirection,
}; };
}, },
}); });
@@ -3,7 +3,10 @@
<LayoutRow :class="'options-bar'"> <LayoutRow :class="'options-bar'">
<NumberInput /> <NumberInput />
<NumberInput /> <NumberInput />
<DropdownButton /> <DropdownButton>
<h3>Compositing Options</h3>
<p>More blend and compositing options will be here</p>
</DropdownButton>
</LayoutRow> </LayoutRow>
<LayoutRow :class="'layer-tree'"> <LayoutRow :class="'layer-tree'">
<LayoutCol :class="'list'"> <LayoutCol :class="'list'">
@@ -78,6 +81,7 @@ import LayoutRow from "../layout/LayoutRow.vue";
import LayoutCol from "../layout/LayoutCol.vue"; import LayoutCol from "../layout/LayoutCol.vue";
import NumberInput from "../widgets/NumberInput.vue"; import NumberInput from "../widgets/NumberInput.vue";
import DropdownButton from "../widgets/DropdownButton.vue"; import DropdownButton from "../widgets/DropdownButton.vue";
import { PopoverDirection } from "../widgets/PopoverMount.vue";
import IconButton from "../widgets/IconButton.vue"; import IconButton from "../widgets/IconButton.vue";
import IconContainer from "../widgets/IconContainer.vue"; import IconContainer from "../widgets/IconContainer.vue";
import EyeVisible from "../../../assets/svg/24x24-bounds-16x16-icon/visibility-eye-visible.svg"; import EyeVisible from "../../../assets/svg/24x24-bounds-16x16-icon/visibility-eye-visible.svg";
@@ -122,6 +126,7 @@ export default defineComponent({
}, },
data() { data() {
return { return {
PopoverDirection,
layers: [] as Array<LayerPanelEntry>, layers: [] as Array<LayerPanelEntry>,
}; };
}, },
@@ -1,25 +1,41 @@
<template> <template>
<button class="dropdown-button"> <div class="dropdown-button">
<component :is="icon" /> <button @click="clickButton">
</button> <component :is="icon" />
</button>
<PopoverMount :direction="PopoverDirection.Bottom" ref="popover">
<slot></slot>
</PopoverMount>
</div>
</template> </template>
<style lang="scss"> <style lang="scss">
.dropdown-button { .dropdown-button {
display: inline-block;
position: relative;
width: 16px; width: 16px;
height: 24px; height: 24px;
margin: 2px 4px; margin: 2px 4px;
padding: 0;
outline: none;
border: none;
border-radius: 2px;
vertical-align: top;
background: #111;
fill: #ddd;
&:hover { .popover-mount {
background: #666; left: 50%;
fill: #fff; }
button {
width: 100%;
height: 100%;
padding: 0;
outline: none;
border: none;
border-radius: 2px;
vertical-align: top;
background: #111;
fill: #ddd;
&:hover {
background: #666;
fill: #fff;
}
} }
} }
</style> </style>
@@ -28,6 +44,7 @@
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import DropdownArrow from "../../../assets/svg/16x24-bounds-8x16-icon/dropdown-arrow.svg"; import DropdownArrow from "../../../assets/svg/16x24-bounds-8x16-icon/dropdown-arrow.svg";
import VerticalEllipsis from "../../../assets/svg/16x24-bounds-8x16-icon/vertical-ellipsis.svg"; import VerticalEllipsis from "../../../assets/svg/16x24-bounds-8x16-icon/vertical-ellipsis.svg";
import PopoverMount, { PopoverDirection } from "./PopoverMount.vue";
export enum DropdownButtonIcon { export enum DropdownButtonIcon {
"DropdownArrow" = "DropdownArrow", "DropdownArrow" = "DropdownArrow",
@@ -38,9 +55,21 @@ export default defineComponent({
components: { components: {
VerticalEllipsis, VerticalEllipsis,
DropdownArrow, DropdownArrow,
PopoverMount,
PopoverDirection,
}, },
props: { props: {
icon: { type: String, default: DropdownButtonIcon.DropdownArrow }, icon: { type: String, default: DropdownButtonIcon.DropdownArrow },
}, },
methods: {
clickButton() {
(this.$refs.popover as typeof PopoverMount).setOpen();
},
},
data() {
return {
PopoverDirection,
};
},
}); });
</script> </script>
@@ -1,7 +1,7 @@
<template> <template>
<div class="popover-mount" v-if="open"> <div class="popover-mount" :class="direction.toLowerCase()" v-if="open">
<div class="tail left"></div> <div class="tail"></div>
<div class="popover"> <div class="popover" ref="popover">
<div class="popover-content" ref="popoverContent"> <div class="popover-content" ref="popoverContent">
<slot></slot> <slot></slot>
</div> </div>
@@ -15,46 +15,75 @@
width: 0; width: 0;
height: 0; height: 0;
display: flex; display: flex;
// Overlays begin at a z-index of 1000
z-index: 1000;
&.top,
&.bottom {
flex-direction: column;
}
} }
.tail { .tail {
width: 0; width: 0;
height: 0; height: 0;
border-style: solid; border-style: solid;
// Put the tail above the popover's shadow
z-index: 1; z-index: 1;
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
position: fixed;
&.top { .top > & {
border-width: 0 6px 8px 6px;
border-color: transparent transparent #222222e6 transparent;
margin-left: -6px;
margin-top: 2px;
}
&.bottom {
border-width: 8px 6px 0 6px; border-width: 8px 6px 0 6px;
border-color: #222222e6 transparent transparent transparent; border-color: #222222e6 transparent transparent transparent;
margin-left: -6px; margin-left: -6px;
margin-bottom: 2px; margin-bottom: 2px;
} }
&.left { .bottom > & {
border-width: 6px 8px 6px 0; border-width: 0 6px 8px 6px;
border-color: transparent #222222e6 transparent transparent; border-color: transparent transparent #222222e6 transparent;
margin-top: -6px; margin-left: -6px;
margin-left: 2px; margin-top: 2px;
} }
&.right { .left > & {
border-width: 6px 0 6px 8px; border-width: 6px 0 6px 8px;
border-color: transparent transparent transparent #222222e6; border-color: transparent transparent transparent #222222e6;
margin-top: -6px; margin-top: -6px;
margin-right: 2px; margin-right: 2px;
} }
.right > & {
border-width: 6px 8px 6px 0;
border-color: transparent #222222e6 transparent transparent;
margin-top: -6px;
margin-left: 2px;
}
} }
.popover { .popover {
display: flex; display: flex;
align-items: center;
.top > & {
justify-content: center;
margin-bottom: 10px;
}
.bottom > & {
justify-content: center;
margin-top: 10px;
}
.left > & {
align-items: center;
margin-right: 10px;
}
.right > & {
align-items: center;
margin-left: 10px;
}
.popover-content { .popover-content {
background: #222222e6; background: #222222e6;
@@ -65,8 +94,9 @@
padding: 8px; padding: 8px;
z-index: 0; z-index: 0;
display: flex; display: flex;
// This `position: relative` is used to allow `top`/`right`/`bottom`/`left` properties to shift the content back from overflowing the workspace flex-direction: column;
position: relative; // Draw over the application without being clipped by the containing panel's `overflow: hidden`
position: fixed;
} }
} }
</style> </style>
@@ -74,24 +104,49 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
export enum PopoverDirection {
Top = "Top",
Bottom = "Bottom",
Left = "Left",
Right = "Right",
}
export default defineComponent({ export default defineComponent({
components: {}, components: {},
props: {
direction: { type: String, default: PopoverDirection.Bottom },
},
data() { data() {
return { return {
open: false, open: false,
mouseStillDown: false, mouseStillDown: false,
PopoverDirection,
}; };
}, },
updated() { updated() {
const popoverContent = this.$refs.popoverContent as HTMLElement; const popoverContent = this.$refs.popoverContent as HTMLElement;
const popover = this.$refs.popover as HTMLElement;
const workspace = document.querySelector(".workspace"); const workspace = document.querySelector(".workspace");
if (popoverContent && workspace) { if (popoverContent && workspace) {
const workspaceBounds = workspace.getBoundingClientRect(); const workspaceBounds = workspace.getBoundingClientRect();
const popoverBounds = popoverContent.getBoundingClientRect(); const popoverBounds = popoverContent.getBoundingClientRect();
const bottomOffset = workspaceBounds.bottom - popoverBounds.bottom - 8; if (this.direction === PopoverDirection.Left || this.direction === PopoverDirection.Right) {
if (bottomOffset < 0) popoverContent.style.top = `${bottomOffset}px`; const topOffset = popoverBounds.top - workspaceBounds.top - 8;
if (topOffset < 0) popover.style.transform = `translate(0, ${-topOffset}px)`;
const bottomOffset = workspaceBounds.bottom - popoverBounds.bottom - 8;
if (bottomOffset < 0) popover.style.transform = `translate(0, ${bottomOffset}px)`;
}
if (this.direction === PopoverDirection.Top || this.direction === PopoverDirection.Bottom) {
const leftOffset = popoverBounds.left - workspaceBounds.left - 8;
if (leftOffset < 0) popover.style.transform = `translate(${-leftOffset}px, 0)`;
const rightOffset = workspaceBounds.right - popoverBounds.right - 8;
if (rightOffset < 0) popover.style.transform = `translate(${rightOffset}px, 0)`;
}
} }
}, },
methods: { methods: {
@@ -6,7 +6,8 @@
<style lang="scss"> <style lang="scss">
.radio-picker { .radio-picker {
button { button,
.dropdown-button {
fill: #ddd; fill: #ddd;
border-radius: 0; border-radius: 0;
margin: 0; margin: 0;
@@ -1,16 +1,18 @@
<template> <template>
<div class="working-colors"> <div class="working-colors">
<div class="swatch-pair"> <div class="swatch-pair">
<button @click="clickSecondarySwatch" class="secondary swatch" style="background: white"> <div class="secondary swatch">
<PopoverMount ref="secondarySwatchPopover"> <button @click="clickSecondarySwatch" style="background: white"></button>
<PopoverMount :direction="PopoverDirection.Right" horizontal ref="secondarySwatchPopover">
<ColorPicker /> <ColorPicker />
</PopoverMount> </PopoverMount>
</button> </div>
<button @click="clickPrimarySwatch" class="primary swatch" style="background: black"> <div class="primary swatch">
<PopoverMount ref="primarySwatchPopover"> <button @click="clickPrimarySwatch" style="background: black"></button>
<PopoverMount :direction="PopoverDirection.Right" horizontal ref="primarySwatchPopover">
<ColorPicker /> <ColorPicker />
</PopoverMount> </PopoverMount>
</button> </div>
</div> </div>
<div class="swap-and-reset"> <div class="swap-and-reset">
<IconButton :size="16"> <IconButton :size="16">
@@ -32,24 +34,31 @@
} }
.swatch { .swatch {
width: 24px; width: 28px;
height: 24px; height: 28px;
border-radius: 50%;
border: 2px #888 solid;
box-shadow: 0 0 0 2px #333;
margin: 2px; margin: 2px;
padding: 0;
box-sizing: unset;
outline: none;
position: relative; position: relative;
.popover-mount { button {
right: -4px; width: 100%;
height: 100%;
border-radius: 50%;
border: 2px #888 solid;
box-shadow: 0 0 0 2px #333;
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none;
} }
}
.primary.swatch { .popover-mount {
margin-bottom: -8px; top: 50%;
right: -2px;
}
&.primary {
margin-bottom: -8px;
}
} }
.swap-and-reset { .swap-and-reset {
@@ -60,7 +69,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import PopoverMount from "./PopoverMount.vue"; import PopoverMount, { PopoverDirection } from "./PopoverMount.vue";
import ColorPicker from "../popovers/ColorPicker.vue"; import ColorPicker from "../popovers/ColorPicker.vue";
import IconButton from "./IconButton.vue"; import IconButton from "./IconButton.vue";
import SwapButton from "../../../assets/svg/16x16-bounds-12x12-icon/swap.svg"; import SwapButton from "../../../assets/svg/16x16-bounds-12x12-icon/swap.svg";
@@ -84,5 +93,10 @@ export default defineComponent({
(this.$refs.primarySwatchPopover as typeof PopoverMount).setClosed(); (this.$refs.primarySwatchPopover as typeof PopoverMount).setClosed();
}, },
}, },
data() {
return {
PopoverDirection,
};
},
}); });
</script> </script>
@@ -9,7 +9,10 @@
</IconButton> </IconButton>
</div> </div>
</div> </div>
<DropdownButton :icon="DropdownButtonIcon.VerticalEllipsis" /> <DropdownButton :icon="DropdownButtonIcon.VerticalEllipsis">
<h3>Panel Options</h3>
<p>More panel-related options will be here</p>
</DropdownButton>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<component :is="panelType" /> <component :is="panelType" />
@@ -54,17 +57,16 @@
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
position: relative; position: relative;
&::before, &:not(:first-child)::before,
&::after { &::after {
content: ""; content: "";
width: 16px; width: 16px;
height: 8px; height: 8px;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
box-shadow: #333;
} }
&::before { &:not(:first-child)::before {
left: -16px; left: -16px;
border-bottom-right-radius: 8px; border-bottom-right-radius: 8px;
box-shadow: 8px 0 0 0 #333; box-shadow: 8px 0 0 0 #333;
@@ -135,6 +137,7 @@ import LayerTree from "../panels/LayerTree.vue";
import Minimap from "../panels/Minimap.vue"; import Minimap from "../panels/Minimap.vue";
import IconButton from "../widgets/IconButton.vue"; import IconButton from "../widgets/IconButton.vue";
import DropdownButton, { DropdownButtonIcon } from "../widgets/DropdownButton.vue"; import DropdownButton, { DropdownButtonIcon } from "../widgets/DropdownButton.vue";
import { PopoverDirection } from "../widgets/PopoverMount.vue";
import VerticalEllipsis from "../../../assets/svg/16x24-bounds-8x16-icon/vertical-ellipsis.svg"; import VerticalEllipsis from "../../../assets/svg/16x24-bounds-8x16-icon/vertical-ellipsis.svg";
import CloseX from "../../../assets/svg/16x16-bounds-12x12-icon/close-x.svg"; import CloseX from "../../../assets/svg/16x16-bounds-12x12-icon/close-x.svg";
@@ -159,6 +162,7 @@ export default defineComponent({
data() { data() {
return { return {
DropdownButtonIcon, DropdownButtonIcon,
PopoverDirection,
}; };
}, },
}); });