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

View File

@@ -1,25 +1,41 @@
<template>
<button class="dropdown-button">
<component :is="icon" />
</button>
<div class="dropdown-button">
<button @click="clickButton">
<component :is="icon" />
</button>
<PopoverMount :direction="PopoverDirection.Bottom" ref="popover">
<slot></slot>
</PopoverMount>
</div>
</template>
<style lang="scss">
.dropdown-button {
display: inline-block;
position: relative;
width: 16px;
height: 24px;
margin: 2px 4px;
padding: 0;
outline: none;
border: none;
border-radius: 2px;
vertical-align: top;
background: #111;
fill: #ddd;
&:hover {
background: #666;
fill: #fff;
.popover-mount {
left: 50%;
}
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>
@@ -28,6 +44,7 @@
import { defineComponent } from "vue";
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 PopoverMount, { PopoverDirection } from "./PopoverMount.vue";
export enum DropdownButtonIcon {
"DropdownArrow" = "DropdownArrow",
@@ -38,9 +55,21 @@ export default defineComponent({
components: {
VerticalEllipsis,
DropdownArrow,
PopoverMount,
PopoverDirection,
},
props: {
icon: { type: String, default: DropdownButtonIcon.DropdownArrow },
},
methods: {
clickButton() {
(this.$refs.popover as typeof PopoverMount).setOpen();
},
},
data() {
return {
PopoverDirection,
};
},
});
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="popover-mount" v-if="open">
<div class="tail left"></div>
<div class="popover">
<div class="popover-mount" :class="direction.toLowerCase()" v-if="open">
<div class="tail"></div>
<div class="popover" ref="popover">
<div class="popover-content" ref="popoverContent">
<slot></slot>
</div>
@@ -15,46 +15,75 @@
width: 0;
height: 0;
display: flex;
// Overlays begin at a z-index of 1000
z-index: 1000;
&.top,
&.bottom {
flex-direction: column;
}
}
.tail {
width: 0;
height: 0;
border-style: solid;
// Put the tail above the popover's shadow
z-index: 1;
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
position: fixed;
&.top {
border-width: 0 6px 8px 6px;
border-color: transparent transparent #222222e6 transparent;
margin-left: -6px;
margin-top: 2px;
}
&.bottom {
.top > & {
border-width: 8px 6px 0 6px;
border-color: #222222e6 transparent transparent transparent;
margin-left: -6px;
margin-bottom: 2px;
}
&.left {
border-width: 6px 8px 6px 0;
border-color: transparent #222222e6 transparent transparent;
margin-top: -6px;
margin-left: 2px;
.bottom > & {
border-width: 0 6px 8px 6px;
border-color: transparent transparent #222222e6 transparent;
margin-left: -6px;
margin-top: 2px;
}
&.right {
.left > & {
border-width: 6px 0 6px 8px;
border-color: transparent transparent transparent #222222e6;
margin-top: -6px;
margin-right: 2px;
}
.right > & {
border-width: 6px 8px 6px 0;
border-color: transparent #222222e6 transparent transparent;
margin-top: -6px;
margin-left: 2px;
}
}
.popover {
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 {
background: #222222e6;
@@ -65,8 +94,9 @@
padding: 8px;
z-index: 0;
display: flex;
// This `position: relative` is used to allow `top`/`right`/`bottom`/`left` properties to shift the content back from overflowing the workspace
position: relative;
flex-direction: column;
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
position: fixed;
}
}
</style>
@@ -74,24 +104,49 @@
<script lang="ts">
import { defineComponent } from "vue";
export enum PopoverDirection {
Top = "Top",
Bottom = "Bottom",
Left = "Left",
Right = "Right",
}
export default defineComponent({
components: {},
props: {
direction: { type: String, default: PopoverDirection.Bottom },
},
data() {
return {
open: false,
mouseStillDown: false,
PopoverDirection,
};
},
updated() {
const popoverContent = this.$refs.popoverContent as HTMLElement;
const popover = this.$refs.popover as HTMLElement;
const workspace = document.querySelector(".workspace");
if (popoverContent && workspace) {
const workspaceBounds = workspace.getBoundingClientRect();
const popoverBounds = popoverContent.getBoundingClientRect();
const bottomOffset = workspaceBounds.bottom - popoverBounds.bottom - 8;
if (bottomOffset < 0) popoverContent.style.top = `${bottomOffset}px`;
if (this.direction === PopoverDirection.Left || this.direction === PopoverDirection.Right) {
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: {

View File

@@ -6,7 +6,8 @@
<style lang="scss">
.radio-picker {
button {
button,
.dropdown-button {
fill: #ddd;
border-radius: 0;
margin: 0;

View File

@@ -1,16 +1,18 @@
<template>
<div class="working-colors">
<div class="swatch-pair">
<button @click="clickSecondarySwatch" class="secondary swatch" style="background: white">
<PopoverMount ref="secondarySwatchPopover">
<div class="secondary swatch">
<button @click="clickSecondarySwatch" style="background: white"></button>
<PopoverMount :direction="PopoverDirection.Right" horizontal ref="secondarySwatchPopover">
<ColorPicker />
</PopoverMount>
</button>
<button @click="clickPrimarySwatch" class="primary swatch" style="background: black">
<PopoverMount ref="primarySwatchPopover">
</div>
<div class="primary swatch">
<button @click="clickPrimarySwatch" style="background: black"></button>
<PopoverMount :direction="PopoverDirection.Right" horizontal ref="primarySwatchPopover">
<ColorPicker />
</PopoverMount>
</button>
</div>
</div>
<div class="swap-and-reset">
<IconButton :size="16">
@@ -32,24 +34,31 @@
}
.swatch {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px #888 solid;
box-shadow: 0 0 0 2px #333;
width: 28px;
height: 28px;
margin: 2px;
padding: 0;
box-sizing: unset;
outline: none;
position: relative;
.popover-mount {
right: -4px;
button {
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 {
margin-bottom: -8px;
.popover-mount {
top: 50%;
right: -2px;
}
&.primary {
margin-bottom: -8px;
}
}
.swap-and-reset {
@@ -60,7 +69,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import PopoverMount from "./PopoverMount.vue";
import PopoverMount, { PopoverDirection } from "./PopoverMount.vue";
import ColorPicker from "../popovers/ColorPicker.vue";
import IconButton from "./IconButton.vue";
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();
},
},
data() {
return {
PopoverDirection,
};
},
});
</script>