Skip to content

Commit ec371d5

Browse files
authored
Merge pull request #29 from qikify/component/popover
popover component
2 parents 8cce8c1 + 092e5c1 commit ec371d5

File tree

26 files changed

+1211
-4
lines changed

26 files changed

+1211
-4
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ module.exports = {
1212
ecmaVersion: 2020,
1313
},
1414
rules: {
15+
'no-shadow': 'off',
16+
'@typescript-eslint/no-shadow': ['error'],
1517
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 0,
1618
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 0,
1719
'import/no-extraneous-dependencies': ['error', {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"minimist": "^1.2.5",
7070
"path": "^0.12.7",
7171
"polaris-react": "https://github.com/Shopify/polaris-react.git",
72+
"portal-vue": "^2.1.7",
7273
"postcss": "^7.0.18",
7374
"postcss-modules": "^3.1.0",
7475
"pug-plain-loader": "^1.1.0",

src/Demo.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
list-item Blue
77

88
icon(:source="icon")
9-
spinner
9+
Popover(
10+
:active="active",
11+
@close="active = !active",
12+
)
13+
button(slot="activator", @click="active = !active") Action
14+
list(type="number", slot="content")
15+
list-item a
16+
list-item b
17+
list-item c
1018
</template>
1119

1220
<script lang="ts">
@@ -16,6 +24,8 @@ import CirclePlusMinor from '@icons/CirclePlusMinor.svg';
1624
@Component
1725
export default class Demo extends Vue {
1826
public icon = CirclePlusMinor;
27+
28+
public active = false;
1929
}
2030
</script>
2131

src/classes/Popover.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"Popover":"Polaris-Popover","PopoverOverlay":"Polaris-Popover__PopoverOverlay","PopoverOverlay-entering":"Polaris-Popover__PopoverOverlay--entering","PopoverOverlay-open":"Polaris-Popover__PopoverOverlay--open","PopoverOverlay-exiting":"Polaris-Popover__PopoverOverlay--exiting","measuring":"Polaris-Popover--measuring","fullWidth":"Polaris-Popover--fullWidth","Content":"Polaris-Popover__Content","positionedAbove":"Polaris-Popover--positionedAbove","Wrapper":"Polaris-Popover__Wrapper","Content-fullHeight":"Polaris-Popover__Content--fullHeight","Content-fluidContent":"Polaris-Popover__Content--fluidContent","Pane":"Polaris-Popover__Pane","Pane-fixed":"Polaris-Popover__Pane--fixed","Section":"Polaris-Popover__Section","FocusTracker":"Polaris-Popover__FocusTracker","PopoverOverlay-hideOnPrint":"Polaris-Popover__PopoverOverlay--hideOnPrint"}

src/classes/PositionedOverlay.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"PositionedOverlay":"Polaris-PositionedOverlay","fixed":"Polaris-PositionedOverlay--fixed","calculating":"Polaris-PositionedOverlay--calculating","preventInteraction":"Polaris-PositionedOverlay--preventInteraction"}

src/components/ChoiceList/ChoiceList.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ export default class ChoiceList extends Vue {
156156
}
157157
158158
public onChange(event: InputEvent, choice: choiceProps): void {
159-
console.log(choice, event);
160159
this.$emit('input', this.updateSelectedChoices(event));
161160
this.$emit('change', choice);
162161
}

src/components/KeypressListener/utils.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// TODO: remove eslint-disable when component popover was updated
2-
// eslint-disable-next-line no-shadow
31
export enum Key {
42
Backspace= 'Backspace',
53
Tab= 'Tab',

src/components/Popover/Popover.vue

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<template lang="pug">
2+
div(ref="container")
3+
slot(name="activator")
4+
Portal(v-if="activatorNode && active", to="popover")
5+
div(:data-portal-id="portalId")
6+
PopoverOverlay(
7+
:activator="activatorNode",
8+
:fullWidth="fullWidth",
9+
:active="active",
10+
:preferInputActivator="preferInputActivator",
11+
:fixed="fixed",
12+
:preferredPosition="preferredPosition",
13+
:preferredAlignment="preferredAlignment",
14+
:zIndexOverride="zIndexOverride",
15+
@close="handleClose",
16+
@scrolled-to-bottom="$emit('scrolled-to-bottom')",
17+
)
18+
template(v-slot:overlay="props")
19+
slot(name="content")
20+
PortalTarget(name="popover")
21+
</template>
22+
23+
<script lang="ts">
24+
import Vue from 'vue';
25+
import {
26+
Component, Prop, Watch, Ref,
27+
} from 'vue-property-decorator';
28+
import type { PreferredAlignment, PreferredPosition } from '../PositionedOverlay';
29+
import {
30+
findFirstFocusableNodeIncludingDisabled,
31+
focusNextFocusableNode,
32+
} from '@/utilities/focus';
33+
import { PopoverCloseSource, PopoverAutofocusTarget, setActivatorAttributes } from './utils';
34+
import { useUniqueId } from '@/utilities/unique-id';
35+
import { PopoverOverlay } from './components';
36+
37+
@Component({
38+
components: {
39+
PopoverOverlay,
40+
},
41+
})
42+
export default class Popover extends Vue {
43+
/**
44+
* The preferred direction to open the popover
45+
* @values above | below | mostSpace
46+
*/
47+
@Prop()
48+
public preferredPosition?: PreferredPosition;
49+
50+
/**
51+
* The preferred alignment of the popover relative to its activator
52+
* @values left | center | right
53+
*/
54+
@Prop()
55+
public preferredAlignment?: PreferredAlignment;
56+
57+
/**
58+
* Show or hide the Popover
59+
*/
60+
@Prop({ type: Boolean, required: true })
61+
public active!: boolean;
62+
63+
/**
64+
* Use the activator's input element to calculate the Popover position
65+
* @default true
66+
*/
67+
@Prop({ type: Boolean, default: true })
68+
public preferInputActivator?: boolean;
69+
70+
/**
71+
* Override on the default z-index of 400
72+
*/
73+
@Prop({ type: Number })
74+
public zIndexOverride?: number;
75+
76+
/**
77+
* Prevents focusing the activator or the next focusable element when the popover is deactivated
78+
*/
79+
@Prop({ type: Boolean })
80+
public preventFocusOnClose?: boolean;
81+
82+
/**
83+
* Automatically add wrap content in a section
84+
*/
85+
@Prop({ type: Boolean })
86+
public sectioned?: boolean;
87+
88+
/**
89+
* Allow popover to stretch to the full width of its activator
90+
*/
91+
@Prop({ type: Boolean })
92+
public fullWidth?: boolean;
93+
94+
/**
95+
* Allow popover to stretch to fit content vertically
96+
*/
97+
@Prop({ type: Boolean })
98+
public fullHeight?: boolean;
99+
100+
/**
101+
* Allow popover content to determine the overlay width and height
102+
*/
103+
@Prop({ type: Boolean })
104+
public fluidContent?: boolean;
105+
106+
/**
107+
* Remains in a fixed position
108+
*/
109+
@Prop({ type: Boolean })
110+
public fixed?: boolean;
111+
112+
/**
113+
/** Used to illustrate the type of popover element
114+
*/
115+
@Prop({ type: String })
116+
public ariaHaspopup!: string;
117+
118+
/**
119+
* Allow the popover overlay to be hidden when printing
120+
*/
121+
@Prop({ type: Boolean })
122+
public hideOnPrint?: boolean;
123+
124+
/**
125+
* The preferred auto focus target defaulting to the popover container
126+
* @default 'container'
127+
*/
128+
@Prop({ type: String, default: 'container' })
129+
public autofocusTarget?: PopoverAutofocusTarget;
130+
131+
@Watch('active')
132+
onActiveChanged() {
133+
this.setAccessibilityAttributes();
134+
}
135+
136+
@Ref('container') containerNode!: HTMLElement;
137+
138+
public isInPortal = (element: Element) => {
139+
let { parentElement } = element;
140+
141+
while (parentElement) {
142+
if (parentElement.matches('.vue-portal-target')) return false;
143+
parentElement = parentElement.parentElement;
144+
}
145+
146+
return true;
147+
}
148+
149+
public handleClose(source: PopoverCloseSource) {
150+
this.$emit('close', source);
151+
if (!this.containerNode || this.preventFocusOnClose) return;
152+
153+
if ((source === PopoverCloseSource.FocusOut || source === PopoverCloseSource.EscapeKeypress)
154+
&& this.activatorNode
155+
) {
156+
const focusableActivator = findFirstFocusableNodeIncludingDisabled(
157+
this.activatorNode as HTMLElement,
158+
)
159+
|| findFirstFocusableNodeIncludingDisabled(this.containerNode)
160+
|| this.containerNode;
161+
if (!focusNextFocusableNode(focusableActivator, this.isInPortal)) {
162+
focusableActivator.focus();
163+
}
164+
}
165+
}
166+
167+
public activatorNode: HTMLElement | Element | null = null;
168+
169+
public id = useUniqueId('popover');
170+
171+
public portalId = `popover-${useUniqueId('portal')}`;
172+
173+
public setAccessibilityAttributes() {
174+
if (!this.containerNode) {
175+
return;
176+
}
177+
178+
const firstFocusable = findFirstFocusableNodeIncludingDisabled(this.containerNode);
179+
const focusableActivator: HTMLElement & { disabled?: boolean; } = firstFocusable
180+
|| this.containerNode;
181+
const activatorDisabled = 'disabled' in focusableActivator && Boolean(focusableActivator.disabled);
182+
183+
setActivatorAttributes(focusableActivator, {
184+
id: this.id,
185+
active: this.active,
186+
ariaHaspopup: this.ariaHaspopup,
187+
activatorDisabled,
188+
});
189+
}
190+
191+
mounted(): void {
192+
if (this.containerNode) {
193+
const activatorNode = this.containerNode.firstElementChild;
194+
if (activatorNode) this.activatorNode = activatorNode;
195+
this.setAccessibilityAttributes();
196+
}
197+
}
198+
}
199+
</script>
200+
<style lang="scss">
201+
@import 'polaris-react/src/components/Popover/Popover.scss';
202+
</style>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
2+
import { Popover, List, ListItem } from '@/polaris-vue';
3+
4+
<Meta
5+
title="Components / Overlays / Popover"
6+
component={ Popover }
7+
argTypes={{
8+
preferredPosition: {
9+
name: 'preferredPosition',
10+
description: 'The preferred direction to open the popover',
11+
options: [
12+
'above',
13+
'below',
14+
'mostSpace'
15+
],
16+
control: { type: 'select' },
17+
table: {
18+
defaultValue: {
19+
summary: 'below'
20+
},
21+
type: {
22+
summary: null,
23+
},
24+
},
25+
},
26+
preferredAlignment: {
27+
name: 'preferredAlignment',
28+
description: 'The preferred alignment of the popover relative to its activator',
29+
options: [
30+
'left',
31+
'center',
32+
'right'
33+
],
34+
control: { type: 'select' },
35+
table: {
36+
defaultValue: {
37+
summary: 'center'
38+
},
39+
type: {
40+
summary: null,
41+
},
42+
},
43+
},
44+
autofocusTarget: {
45+
name: 'autofocusTarget',
46+
description: 'The preferred auto focus target defaulting to the popover container',
47+
options: [
48+
'container',
49+
'none',
50+
'first-node'
51+
],
52+
control: { type: 'select' },
53+
table: {
54+
defaultValue: {
55+
summary: 'container'
56+
},
57+
type: {
58+
summary: null,
59+
},
60+
},
61+
},
62+
default: {
63+
table: {
64+
disable: true,
65+
},
66+
},
67+
}}
68+
/>
69+
70+
export const Template = (args, { argTypes }) => ({
71+
props: Object.keys(argTypes),
72+
components: { Popover, List, ListItem },
73+
template: `
74+
<Popover v-bind="$props">
75+
<button slot="activator" @click="active = !active">Action</button>
76+
<List type="number" slot="content">
77+
<ListItem>Yellow shirt</ListItem>
78+
<ListItem>Red shirt</ListItem>
79+
<ListItem>Green shirt</ListItem>
80+
</Popover>
81+
`,
82+
});
83+
84+
# Popover
85+
86+
Popovers are small overlays that open on demand. They let merchants access additional content and actions without cluttering the page.
87+
88+
<Canvas>
89+
<Story
90+
name="Popover"
91+
args={{
92+
active: false,
93+
}}
94+
height="300px"
95+
parameters={{
96+
docs: {
97+
transformSource: (source) => {
98+
const tmpSource = source
99+
.replace(/<template>/, '')
100+
.replace(/<\/template>/, '');
101+
return tmpSource;
102+
},
103+
},
104+
}}
105+
>
106+
{Template.bind({})}
107+
</Story>
108+
</Canvas>
109+
110+
<ArgsTable story="Popover" />

0 commit comments

Comments
 (0)