diff --git a/.storybook/config.js b/.storybook/config.js index 76e13288..ddc3a97f 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -18,7 +18,10 @@ import { faBolt, faUserSlash, faCheckCircle, - faCog } from '@fortawesome/free-solid-svg-icons' + faCog, + faLock, + faEye, + faEyeSlash } from '@fortawesome/free-solid-svg-icons' import { faChevronCircleUp, @@ -48,7 +51,10 @@ Vue.use(Kiwi, { faTimesCircle, faGithub, faCheckCircle, - faCog + faCog, + faLock, + faEye, + faEyeSlash } } }) diff --git a/package.json b/package.json index 16f078c3..2bd24f94 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "test": "jest", "clean": "lerna run clean", "release": "lerna publish", + "patch": "lerna version patch", "push": "git add -A && git commit -m \"chore(lerna): publishing package \" && lerna version --conventional-commits", "storybook": "start-storybook -p 9000", "build-storybook": "build-storybook --quiet", diff --git a/packages/chakra-ui-core/src/Accordion/Accordion.js b/packages/chakra-ui-core/src/Accordion/Accordion.js new file mode 100644 index 00000000..1af0171c --- /dev/null +++ b/packages/chakra-ui-core/src/Accordion/Accordion.js @@ -0,0 +1,313 @@ +import { baseProps } from '../config' +import Box from '../Box' +import { forwardProps, cloneVNodes, useId, isDef } from '../utils' +import PseudoBox from '../PseudoBox' +import styleProps from '../config/props' +import Collapse from '../Collapse' +import Icon from '../Icon' +import { iconProps } from '../Icon/icon.props' + +const Accordion = { + name: 'Accordion', + props: { + ...baseProps, + allowMultiple: Boolean, + allowToggle: Boolean, + index: { + type: Number, + default: null + }, + defaultIndex: { + type: [Array, Number], + default: 0 + } + }, + data () { + const initializeState = () => { + if (this.allowMultiple) { + return this.defaultIndex || [] + } else { + return this.defaultIndex || 0 + } + } + return { + expandedIndex: initializeState() + } + }, + computed: { + isControlled () { + return this.index != null + }, + _index: { + get () { + return this.isControlled ? this.index : this.expandedIndex + }, + set (val) { + this.expandedIndex = val + } + } + }, + methods: { + getExpandCondition (index, itemIndex) { + if (Array.isArray(index)) { + return index.includes(itemIndex) + } + return index === itemIndex + } + }, + render (h) { + const children = this.$slots.default.filter(e => e.tag) + const cloned = cloneVNodes(children, h) + const clones = cloned.map((vnode, index) => { + const clone = h(vnode.componentOptions.Ctor, { + ...vnode.data, + ...(vnode.componentOptions.listeners || {}), + props: { + ...(vnode.data.props || {}), + ...vnode.componentOptions.propsData, + isOpen: this.getExpandCondition(this._index, index) + }, + on: { + change: (isExpanded) => { + if (this.allowMultiple) { + if (isExpanded) { + let newIndices = [...this._index, index] + if (!this.isControlled) { + this.expandedIndex = newIndices + }; + this.$emit('change', newIndices) + } else { + let newIndices = this._index.filter( + itemIndex => itemIndex !== index + ) + if (!this.isControlled) { + this.expandedIndex = newIndices + }; + this.$emit('change', newIndices) + } + } else { + if (isExpanded) { + if (!this.isControlled) { + this.expandedIndex = index + }; + this.$emit('change', index) + } else { + if (this.allowToggle) { + if (!this.isControlled) { + this.expandedIndex = null + }; + this.$emit('change', null) + } + } + } + } + } + }, vnode.componentOptions.children) + return clone + }) + + return h(Box, { + props: forwardProps(this.$props), + attrs: { + 'data-accordion': '' + } + }, clones) + } +} + +const AccordionItem = { + name: 'AccordionItem', + props: { + ...styleProps, + isOpen: { + type: Boolean, + default: null + }, + defaultIsOpen: { + type: Boolean, + default: false + }, + id: { + type: String, + default: useId() + }, + isDisabled: { + type: Boolean, + default: false + } + }, + provide () { + return { + $AccordionContext: () => this.AccordionContext + } + }, + data () { + return { + isExpanded: this.defaultIsOpen || false + } + }, + computed: { + AccordionContext () { + return { + isExpanded: this._isExpanded, + isDisabled: this.isDisabled, + headerId: this.headerId, + panelId: this.panelId, + onToggle: this.onToggle + } + }, + isControlled () { + return isDef(this.isOpen) + }, + _isExpanded: { + get () { + return this.isControlled ? this.isOpen : this.isExpanded + }, + set (value) { + this.isExpanded = value + } + }, + headerId () { + return `accordion-header-${this.id}` + }, + panelId () { + return `accordion-panel-${this.id}` + } + }, + methods: { + onToggle () { + this.$emit('change', !this._isExpanded) + if (!this.isControlled) { + this._isExpanded = !this._isExpanded + } + } + }, + render (h) { + return h(PseudoBox, { + props: { + ...forwardProps(this.$props), + borderTopWidth: '1px', + _last: { borderBottomWidth: '1px' } + }, + attrs: { + 'data-accordion-item': '' + } + }, [ + this.$scopedSlots.default({ + isExpanded: this._isExpanded, + isDisabled: this.isDisabled + }) + ]) + } +} + +const AccordionHeader = { + name: 'AccordionHeader', + inject: ['$AccordionContext'], + props: styleProps, + computed: { + context () { + return this.$AccordionContext() + } + }, + render (h) { + const { isExpanded, panelId, headerId, isDisabled, onToggle } = this.context + return h(PseudoBox, { + props: { + ...forwardProps(this.$props), + as: 'button', + display: 'flex', + alignItems: 'center', + width: '100%', + outline: 0, + transition: 'all 0.2s', + px: 4, + py: 2, + _focus: { boxShadow: 'outline' }, + _hover: { bg: 'blackAlpha.50' }, + _disabled: { opacity: '0.4', cursor: 'not-allowed' } + }, + attrs: { + id: headerId, + type: 'button', + disabled: isDisabled, + 'aria-disabled': isDisabled, + 'aria-expanded': isExpanded, + 'aria-controls': panelId + }, + nativeOn: { + click: (e) => { + onToggle() + this.$emit('click', e) + } + } + }, this.$slots.default) + } +} + +const AccordionPanel = { + name: 'AccordionPanel', + inject: ['$AccordionContext'], + props: baseProps, + computed: { + context () { + return this.$AccordionContext() + } + }, + render (h) { + const { isExpanded, panelId, headerId } = this.context + + return h(Collapse, { + props: { + ...forwardProps(this.$props), + isOpen: isExpanded, + pt: 2, + px: 4, + pb: 5 + }, + attrs: { + id: panelId, + 'data-accordion-panel': '', + 'aria-labelledby': headerId, + 'aria-hidden': !isExpanded, + role: 'region' + } + }, this.$slots.default) + } +} + +const AccordionIcon = { + name: 'AccordionIcon', + inject: ['$AccordionContext'], + props: { + ...baseProps, + ...iconProps + }, + computed: { + context () { + return this.$AccordionContext() + } + }, + render (h) { + const { isExpanded, isDisabled } = this.context + return h(Icon, { + props: { + ...forwardProps(this.$props), + size: this.size || '1.25em', + name: this.name || 'chevron-down', + opacity: isDisabled ? 0.4 : 1, + transform: isExpanded ? 'rotate(-180deg)' : null, + transition: 'transform 0.2s', + transformOrigin: 'center' + } + }) + } +} + +export { + Accordion, + AccordionItem, + AccordionHeader, + AccordionPanel, + AccordionIcon +} diff --git a/packages/chakra-ui-core/src/Accordion/index.js b/packages/chakra-ui-core/src/Accordion/index.js index 1af0171c..5d69eaa8 100644 --- a/packages/chakra-ui-core/src/Accordion/index.js +++ b/packages/chakra-ui-core/src/Accordion/index.js @@ -1,313 +1 @@ -import { baseProps } from '../config' -import Box from '../Box' -import { forwardProps, cloneVNodes, useId, isDef } from '../utils' -import PseudoBox from '../PseudoBox' -import styleProps from '../config/props' -import Collapse from '../Collapse' -import Icon from '../Icon' -import { iconProps } from '../Icon/icon.props' - -const Accordion = { - name: 'Accordion', - props: { - ...baseProps, - allowMultiple: Boolean, - allowToggle: Boolean, - index: { - type: Number, - default: null - }, - defaultIndex: { - type: [Array, Number], - default: 0 - } - }, - data () { - const initializeState = () => { - if (this.allowMultiple) { - return this.defaultIndex || [] - } else { - return this.defaultIndex || 0 - } - } - return { - expandedIndex: initializeState() - } - }, - computed: { - isControlled () { - return this.index != null - }, - _index: { - get () { - return this.isControlled ? this.index : this.expandedIndex - }, - set (val) { - this.expandedIndex = val - } - } - }, - methods: { - getExpandCondition (index, itemIndex) { - if (Array.isArray(index)) { - return index.includes(itemIndex) - } - return index === itemIndex - } - }, - render (h) { - const children = this.$slots.default.filter(e => e.tag) - const cloned = cloneVNodes(children, h) - const clones = cloned.map((vnode, index) => { - const clone = h(vnode.componentOptions.Ctor, { - ...vnode.data, - ...(vnode.componentOptions.listeners || {}), - props: { - ...(vnode.data.props || {}), - ...vnode.componentOptions.propsData, - isOpen: this.getExpandCondition(this._index, index) - }, - on: { - change: (isExpanded) => { - if (this.allowMultiple) { - if (isExpanded) { - let newIndices = [...this._index, index] - if (!this.isControlled) { - this.expandedIndex = newIndices - }; - this.$emit('change', newIndices) - } else { - let newIndices = this._index.filter( - itemIndex => itemIndex !== index - ) - if (!this.isControlled) { - this.expandedIndex = newIndices - }; - this.$emit('change', newIndices) - } - } else { - if (isExpanded) { - if (!this.isControlled) { - this.expandedIndex = index - }; - this.$emit('change', index) - } else { - if (this.allowToggle) { - if (!this.isControlled) { - this.expandedIndex = null - }; - this.$emit('change', null) - } - } - } - } - } - }, vnode.componentOptions.children) - return clone - }) - - return h(Box, { - props: forwardProps(this.$props), - attrs: { - 'data-accordion': '' - } - }, clones) - } -} - -const AccordionItem = { - name: 'AccordionItem', - props: { - ...styleProps, - isOpen: { - type: Boolean, - default: null - }, - defaultIsOpen: { - type: Boolean, - default: false - }, - id: { - type: String, - default: useId() - }, - isDisabled: { - type: Boolean, - default: false - } - }, - provide () { - return { - $AccordionContext: () => this.AccordionContext - } - }, - data () { - return { - isExpanded: this.defaultIsOpen || false - } - }, - computed: { - AccordionContext () { - return { - isExpanded: this._isExpanded, - isDisabled: this.isDisabled, - headerId: this.headerId, - panelId: this.panelId, - onToggle: this.onToggle - } - }, - isControlled () { - return isDef(this.isOpen) - }, - _isExpanded: { - get () { - return this.isControlled ? this.isOpen : this.isExpanded - }, - set (value) { - this.isExpanded = value - } - }, - headerId () { - return `accordion-header-${this.id}` - }, - panelId () { - return `accordion-panel-${this.id}` - } - }, - methods: { - onToggle () { - this.$emit('change', !this._isExpanded) - if (!this.isControlled) { - this._isExpanded = !this._isExpanded - } - } - }, - render (h) { - return h(PseudoBox, { - props: { - ...forwardProps(this.$props), - borderTopWidth: '1px', - _last: { borderBottomWidth: '1px' } - }, - attrs: { - 'data-accordion-item': '' - } - }, [ - this.$scopedSlots.default({ - isExpanded: this._isExpanded, - isDisabled: this.isDisabled - }) - ]) - } -} - -const AccordionHeader = { - name: 'AccordionHeader', - inject: ['$AccordionContext'], - props: styleProps, - computed: { - context () { - return this.$AccordionContext() - } - }, - render (h) { - const { isExpanded, panelId, headerId, isDisabled, onToggle } = this.context - return h(PseudoBox, { - props: { - ...forwardProps(this.$props), - as: 'button', - display: 'flex', - alignItems: 'center', - width: '100%', - outline: 0, - transition: 'all 0.2s', - px: 4, - py: 2, - _focus: { boxShadow: 'outline' }, - _hover: { bg: 'blackAlpha.50' }, - _disabled: { opacity: '0.4', cursor: 'not-allowed' } - }, - attrs: { - id: headerId, - type: 'button', - disabled: isDisabled, - 'aria-disabled': isDisabled, - 'aria-expanded': isExpanded, - 'aria-controls': panelId - }, - nativeOn: { - click: (e) => { - onToggle() - this.$emit('click', e) - } - } - }, this.$slots.default) - } -} - -const AccordionPanel = { - name: 'AccordionPanel', - inject: ['$AccordionContext'], - props: baseProps, - computed: { - context () { - return this.$AccordionContext() - } - }, - render (h) { - const { isExpanded, panelId, headerId } = this.context - - return h(Collapse, { - props: { - ...forwardProps(this.$props), - isOpen: isExpanded, - pt: 2, - px: 4, - pb: 5 - }, - attrs: { - id: panelId, - 'data-accordion-panel': '', - 'aria-labelledby': headerId, - 'aria-hidden': !isExpanded, - role: 'region' - } - }, this.$slots.default) - } -} - -const AccordionIcon = { - name: 'AccordionIcon', - inject: ['$AccordionContext'], - props: { - ...baseProps, - ...iconProps - }, - computed: { - context () { - return this.$AccordionContext() - } - }, - render (h) { - const { isExpanded, isDisabled } = this.context - return h(Icon, { - props: { - ...forwardProps(this.$props), - size: this.size || '1.25em', - name: this.name || 'chevron-down', - opacity: isDisabled ? 0.4 : 1, - transform: isExpanded ? 'rotate(-180deg)' : null, - transition: 'transform 0.2s', - transformOrigin: 'center' - } - }) - } -} - -export { - Accordion, - AccordionItem, - AccordionHeader, - AccordionPanel, - AccordionIcon -} +export * from './Accordion' diff --git a/packages/chakra-ui-core/src/Alert/Alert.js b/packages/chakra-ui-core/src/Alert/Alert.js new file mode 100644 index 00000000..13138b7a --- /dev/null +++ b/packages/chakra-ui-core/src/Alert/Alert.js @@ -0,0 +1,123 @@ +import Box from '../Box' +import Icon from '../Icon' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' +import useAlertStyle, { useAlertIconStyle } from './alert.styles' + +export const statuses = { + info: { icon: '_info', color: 'blue' }, + warning: { icon: '_warning-2', color: 'orange' }, + success: { icon: '_check-circle', color: 'green' }, + error: { icon: '_warning', color: 'red' } +} + +const Alert = { + name: 'Alert', + inject: ['$theme', '$colorMode'], + provide () { + return { + _status: this.status, + _variant: this.variant + } + }, + computed: { + colorMode () { + return this.$colorMode() + } + }, + props: { + status: { + type: [String, Array], + default: 'info' + }, + variant: { + type: [String, Array], + default: 'subtle' + }, + ...baseProps + }, + render (h) { + const alertStyles = useAlertStyle({ + variant: this.variant, + color: statuses[this.status] && statuses[this.status]['color'], + colorMode: this.colorMode, + theme: this.$theme() + }) + + return h(Box, { + props: { + fontFamily: 'body', + ...alertStyles, + ...forwardProps(this.$props) + }, + attrs: { + role: 'alert' + } + }, this.$slots.default) + } +} + +const AlertIcon = { + name: 'AlertIcon', + inject: ['_status', '_variant', '$colorMode', '$theme'], + props: { + size: { + default: 5 + }, + name: String, + ...baseProps + }, + computed: { + colorMode () { + return this.$colorMode() + } + }, + render (h) { + const alertIconStyles = useAlertIconStyle({ + variant: this._variant, + colorMode: this.colorMode, + color: statuses[this._status] && statuses[this._status]['color'] + }) + + return h(Icon, { + props: { + mr: this.$props.mr || 3, + size: this.size, + name: this.name || (statuses[this._status] && statuses[this._status]['icon']), + ...alertIconStyles, + ...forwardProps(this.$props) + }, + attrs: { + focusable: false + } + }) + } +} + +const AlertTitle = { + name: 'AlertTitle', + props: { + ...baseProps + }, + render (h) { + return h(Box, { + props: { + fontWeight: 'bold', + lineHeight: 'normal', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const AlertDescription = { + name: 'AlertDescription', + props: { + ...baseProps + }, + render (h) { + return h(Box, { props: forwardProps(this.$props) }, this.$slots.default) + } +} + +export { Alert, AlertIcon, AlertTitle, AlertDescription } diff --git a/packages/chakra-ui-core/src/Alert/index.js b/packages/chakra-ui-core/src/Alert/index.js index 13138b7a..1d5a733b 100644 --- a/packages/chakra-ui-core/src/Alert/index.js +++ b/packages/chakra-ui-core/src/Alert/index.js @@ -1,123 +1 @@ -import Box from '../Box' -import Icon from '../Icon' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' -import useAlertStyle, { useAlertIconStyle } from './alert.styles' - -export const statuses = { - info: { icon: '_info', color: 'blue' }, - warning: { icon: '_warning-2', color: 'orange' }, - success: { icon: '_check-circle', color: 'green' }, - error: { icon: '_warning', color: 'red' } -} - -const Alert = { - name: 'Alert', - inject: ['$theme', '$colorMode'], - provide () { - return { - _status: this.status, - _variant: this.variant - } - }, - computed: { - colorMode () { - return this.$colorMode() - } - }, - props: { - status: { - type: [String, Array], - default: 'info' - }, - variant: { - type: [String, Array], - default: 'subtle' - }, - ...baseProps - }, - render (h) { - const alertStyles = useAlertStyle({ - variant: this.variant, - color: statuses[this.status] && statuses[this.status]['color'], - colorMode: this.colorMode, - theme: this.$theme() - }) - - return h(Box, { - props: { - fontFamily: 'body', - ...alertStyles, - ...forwardProps(this.$props) - }, - attrs: { - role: 'alert' - } - }, this.$slots.default) - } -} - -const AlertIcon = { - name: 'AlertIcon', - inject: ['_status', '_variant', '$colorMode', '$theme'], - props: { - size: { - default: 5 - }, - name: String, - ...baseProps - }, - computed: { - colorMode () { - return this.$colorMode() - } - }, - render (h) { - const alertIconStyles = useAlertIconStyle({ - variant: this._variant, - colorMode: this.colorMode, - color: statuses[this._status] && statuses[this._status]['color'] - }) - - return h(Icon, { - props: { - mr: this.$props.mr || 3, - size: this.size, - name: this.name || (statuses[this._status] && statuses[this._status]['icon']), - ...alertIconStyles, - ...forwardProps(this.$props) - }, - attrs: { - focusable: false - } - }) - } -} - -const AlertTitle = { - name: 'AlertTitle', - props: { - ...baseProps - }, - render (h) { - return h(Box, { - props: { - fontWeight: 'bold', - lineHeight: 'normal', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const AlertDescription = { - name: 'AlertDescription', - props: { - ...baseProps - }, - render (h) { - return h(Box, { props: forwardProps(this.$props) }, this.$slots.default) - } -} - -export { Alert, AlertIcon, AlertTitle, AlertDescription } +export * from './Alert' diff --git a/packages/chakra-ui-core/src/AlertDialog/AlertDialog.js b/packages/chakra-ui-core/src/AlertDialog/AlertDialog.js new file mode 100644 index 00000000..0ef60fe2 --- /dev/null +++ b/packages/chakra-ui-core/src/AlertDialog/AlertDialog.js @@ -0,0 +1,67 @@ +import baseProps from '../config/props' +import { forwardProps, HTMLElement } from '../utils' +import { + Modal, + ModalContent, + ModalFooter, + ModalBody, + ModalHeader, + ModalOverlay, + ModalCloseButton +} from '../Modal' + +const formatIds = id => ({ + content: `alert-dialog-${id}`, + header: `alert-dialog-${id}-label`, + body: `alert-dialog-${id}-desc` +}) + +const AlertDialog = { + name: 'AlertDialog', + props: { + isOpen: { + type: Boolean, + default: false + }, + onClose: { + type: Function, + default: () => null + }, + leastDestructiveRef: [HTMLElement, Object], + ...baseProps + }, + render (h) { + return h(Modal, { + props: { + isOpen: this.isOpen, + onClose: this.onClose, + initialFocusRef: this.leastDestructiveRef, + formatIds, + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const AlertDialogContent = { + name: 'AlertDialogContent', + props: baseProps, + render (h) { + return h(ModalContent, { + props: forwardProps(this.$props), + attrs: { + role: 'alertdialog' + } + }, this.$slots.default) + } +} + +export { + AlertDialog, + AlertDialogContent, + ModalOverlay as AlertDialogOverlay, + ModalBody as AlertDialogBody, + ModalHeader as AlertDialogHeader, + ModalFooter as AlertDialogFooter, + ModalCloseButton as AlertDialogCloseButton +} diff --git a/packages/chakra-ui-core/src/AlertDialog/index.js b/packages/chakra-ui-core/src/AlertDialog/index.js index 0ef60fe2..67bb5e91 100644 --- a/packages/chakra-ui-core/src/AlertDialog/index.js +++ b/packages/chakra-ui-core/src/AlertDialog/index.js @@ -1,67 +1 @@ -import baseProps from '../config/props' -import { forwardProps, HTMLElement } from '../utils' -import { - Modal, - ModalContent, - ModalFooter, - ModalBody, - ModalHeader, - ModalOverlay, - ModalCloseButton -} from '../Modal' - -const formatIds = id => ({ - content: `alert-dialog-${id}`, - header: `alert-dialog-${id}-label`, - body: `alert-dialog-${id}-desc` -}) - -const AlertDialog = { - name: 'AlertDialog', - props: { - isOpen: { - type: Boolean, - default: false - }, - onClose: { - type: Function, - default: () => null - }, - leastDestructiveRef: [HTMLElement, Object], - ...baseProps - }, - render (h) { - return h(Modal, { - props: { - isOpen: this.isOpen, - onClose: this.onClose, - initialFocusRef: this.leastDestructiveRef, - formatIds, - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const AlertDialogContent = { - name: 'AlertDialogContent', - props: baseProps, - render (h) { - return h(ModalContent, { - props: forwardProps(this.$props), - attrs: { - role: 'alertdialog' - } - }, this.$slots.default) - } -} - -export { - AlertDialog, - AlertDialogContent, - ModalOverlay as AlertDialogOverlay, - ModalBody as AlertDialogBody, - ModalHeader as AlertDialogHeader, - ModalFooter as AlertDialogFooter, - ModalCloseButton as AlertDialogCloseButton -} +export * from './AlertDialog' diff --git a/packages/chakra-ui-core/src/AspectRatioBox/AspectRatioBox.js b/packages/chakra-ui-core/src/AspectRatioBox/AspectRatioBox.js new file mode 100644 index 00000000..1033f401 --- /dev/null +++ b/packages/chakra-ui-core/src/AspectRatioBox/AspectRatioBox.js @@ -0,0 +1,48 @@ +import styleProps from '../config/props' +import PseudoBox from '../PseudoBox' +import { cloneVNode, forwardProps } from '../utils' + +const AspectRatioBox = { + name: 'AspectRatioBox', + props: { + ...styleProps, + ratio: { + type: Number, + default: 4 / 3 + } + }, + render (h) { + const child = this.$slots.default[0] + if (!child) return + let vnode = cloneVNode(child, h) + const clone = h(vnode.componentOptions.Ctor, { + ...vnode.data, + ...(vnode.componentOptions.listeners || {}), + props: { + ...(vnode.data.props || {}), + ...vnode.componentOptions.propsData, + position: 'absolute', + w: 'full', + h: 'full', + top: 0, + left: 0 + } + }) + + return h(PseudoBox, { + props: { + ...forwardProps(this.$props), + position: 'relative', + _before: { + h: '0px', + content: `""`, + d: 'block', + pb: `${(1 / this.ratio) * 100}%` + } + }, + attrs: this.$attrs + }, [clone]) + } +} + +export default AspectRatioBox diff --git a/packages/chakra-ui-core/src/AspectRatioBox/index.js b/packages/chakra-ui-core/src/AspectRatioBox/index.js index 1033f401..6eeae1ee 100644 --- a/packages/chakra-ui-core/src/AspectRatioBox/index.js +++ b/packages/chakra-ui-core/src/AspectRatioBox/index.js @@ -1,48 +1,2 @@ -import styleProps from '../config/props' -import PseudoBox from '../PseudoBox' -import { cloneVNode, forwardProps } from '../utils' - -const AspectRatioBox = { - name: 'AspectRatioBox', - props: { - ...styleProps, - ratio: { - type: Number, - default: 4 / 3 - } - }, - render (h) { - const child = this.$slots.default[0] - if (!child) return - let vnode = cloneVNode(child, h) - const clone = h(vnode.componentOptions.Ctor, { - ...vnode.data, - ...(vnode.componentOptions.listeners || {}), - props: { - ...(vnode.data.props || {}), - ...vnode.componentOptions.propsData, - position: 'absolute', - w: 'full', - h: 'full', - top: 0, - left: 0 - } - }) - - return h(PseudoBox, { - props: { - ...forwardProps(this.$props), - position: 'relative', - _before: { - h: '0px', - content: `""`, - d: 'block', - pb: `${(1 / this.ratio) * 100}%` - } - }, - attrs: this.$attrs - }, [clone]) - } -} - +import AspectRatioBox from './AspectRatioBox' export default AspectRatioBox diff --git a/packages/chakra-ui-core/src/Avatar/Avatar.js b/packages/chakra-ui-core/src/Avatar/Avatar.js new file mode 100644 index 00000000..d0dd6650 --- /dev/null +++ b/packages/chakra-ui-core/src/Avatar/Avatar.js @@ -0,0 +1,245 @@ +import { baseProps } from '../config/props' +import { forwardProps, canUseDOM } from '../utils/' +import useAvatarStyles, { avatarSizes } from './avatar.styles' +import Box from '../Box' + +/** + * @description Generate Avatar initials from name string + * @param {String} name + * @returns {String} Avatar Initials + */ +const getInitials = (name) => { + let [firstName, lastName] = name.split(' ') + + if (firstName && lastName) { + return `${firstName.charAt(0)}${lastName.charAt(0)}` + } else { + return firstName.charAt(0) + } +} + +/** + * Avatar badge show's avatar status + */ +export const AvatarBadge = { + name: 'AvatarBadge', + inject: ['$theme', '$colorMode'], + props: { + size: [String, Number, Array], + ...baseProps + }, + computed: { + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + } + }, + render (h) { + const borderColorStyle = { light: 'white', dark: 'gray.800' } + return h(Box, { + props: { + w: this.size, + h: this.size, + position: 'absolute', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transform: 'translate(25%, 25%)', + bottom: '0', + right: '0', + border: '0.2em solid', + borderColor: borderColorStyle[this.colorMode], + rounded: 'full', + ...forwardProps(this.$props) + } + }) + } +} + +/** + * Avatar name displays an Avatar with name initials + */ +const AvatarName = { + name: 'AvatarName', + props: { + name: [String, Array], + size: [String, Array], + ...baseProps + }, + render (h) { + return h(Box, { + props: { + w: this.size, + h: this.size, + textAlign: 'center', + textTransform: 'uppercase', + fontWeight: 'medium', + ...forwardProps(this.$props) + }, + attrs: { + 'aria-label': this.name + } + }, [this.name && getInitials(this.name)]) + } +} + +/** + * Default Avatar component shows fallback image of headshots + */ +const DefaultAvatar = { + name: 'DefaultAvatar', + props: { + size: [String, Number, Array], + ...baseProps + }, + render (h) { + return h(Box, { + props: { + h: this.size, + w: this.size, + lineHeight: '1rem', + ...forwardProps(this.$props) + }, + domProps: { + innerHTML: ` + + + + + + + ` + } + }) + } +} + +/** + * Avatar component shows images of headshots + */ +export const Avatar = { + name: 'Avatar', + inject: ['$theme', '$colorMode'], + computed: { + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + } + }, + props: { + size: { + type: String, + default: 'md' + }, + showBorder: { + type: Boolean, + default: true + }, + name: [String, Array], + src: [String, Array], + borderColor: [String], + ...baseProps + }, + data () { + return { + image: undefined, + hasLoaded: false + } + }, + created () { + // Should only invoke window.Image in the browser. + if (process.browser) { + this.loadImage(this.src) + } + }, + methods: { + loadImage (src) { + if (!canUseDOM) { + return + } + + const image = new window.Image() + image.src = src + + image.onload = event => { + this.hasLoaded = true + this.$emit('load', event) + } + + image.onError = event => { + this.hasLoaded = false + this.$emit('error', event) + } + } + }, + render (h) { + const avatarStyleProps = useAvatarStyles({ + size: this.size, + name: this.name, + showBorder: this.showBorder, + borderColor: this.borderColor, + theme: this.theme, + colorMode: this.colorMode + }) + + const theme = this.theme + const sizeKey = avatarSizes[this.size] + const _size = theme.sizes[sizeKey] + const fontSize = `calc(${_size} / 2.5)` + + /** + * @description Render child nodes for avatar + * @returns {Vue.VNode} + */ + const renderChildren = () => { + if (this.src && this.hasLoaded) { + return h(Box, { + props: { + as: 'img', + w: '100%', + h: '100%', + rounded: 'full', + objectFit: 'cover', + alt: this.name + }, + attrs: { + src: this.src + } + }) + } + + if (this.src && !this.hasLoaded) { + if (this.name) { + return h(AvatarName, { + props: { + name: this.name, + size: _size + } + }) + } else { + return h(DefaultAvatar, { + attrs: { + 'aria-label': this.name + } + }) + } + } + } + + return h(Box, { + props: { + fontSize: fontSize, + lineHeight: _size, + ...avatarStyleProps, + ...forwardProps(this.$props) + } + }, [ + renderChildren(), + this.$slots.default + ]) + } +} diff --git a/packages/chakra-ui-core/src/Avatar/index.js b/packages/chakra-ui-core/src/Avatar/index.js index d0dd6650..227ecdba 100644 --- a/packages/chakra-ui-core/src/Avatar/index.js +++ b/packages/chakra-ui-core/src/Avatar/index.js @@ -1,245 +1 @@ -import { baseProps } from '../config/props' -import { forwardProps, canUseDOM } from '../utils/' -import useAvatarStyles, { avatarSizes } from './avatar.styles' -import Box from '../Box' - -/** - * @description Generate Avatar initials from name string - * @param {String} name - * @returns {String} Avatar Initials - */ -const getInitials = (name) => { - let [firstName, lastName] = name.split(' ') - - if (firstName && lastName) { - return `${firstName.charAt(0)}${lastName.charAt(0)}` - } else { - return firstName.charAt(0) - } -} - -/** - * Avatar badge show's avatar status - */ -export const AvatarBadge = { - name: 'AvatarBadge', - inject: ['$theme', '$colorMode'], - props: { - size: [String, Number, Array], - ...baseProps - }, - computed: { - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - } - }, - render (h) { - const borderColorStyle = { light: 'white', dark: 'gray.800' } - return h(Box, { - props: { - w: this.size, - h: this.size, - position: 'absolute', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - transform: 'translate(25%, 25%)', - bottom: '0', - right: '0', - border: '0.2em solid', - borderColor: borderColorStyle[this.colorMode], - rounded: 'full', - ...forwardProps(this.$props) - } - }) - } -} - -/** - * Avatar name displays an Avatar with name initials - */ -const AvatarName = { - name: 'AvatarName', - props: { - name: [String, Array], - size: [String, Array], - ...baseProps - }, - render (h) { - return h(Box, { - props: { - w: this.size, - h: this.size, - textAlign: 'center', - textTransform: 'uppercase', - fontWeight: 'medium', - ...forwardProps(this.$props) - }, - attrs: { - 'aria-label': this.name - } - }, [this.name && getInitials(this.name)]) - } -} - -/** - * Default Avatar component shows fallback image of headshots - */ -const DefaultAvatar = { - name: 'DefaultAvatar', - props: { - size: [String, Number, Array], - ...baseProps - }, - render (h) { - return h(Box, { - props: { - h: this.size, - w: this.size, - lineHeight: '1rem', - ...forwardProps(this.$props) - }, - domProps: { - innerHTML: ` - - - - - - - ` - } - }) - } -} - -/** - * Avatar component shows images of headshots - */ -export const Avatar = { - name: 'Avatar', - inject: ['$theme', '$colorMode'], - computed: { - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - } - }, - props: { - size: { - type: String, - default: 'md' - }, - showBorder: { - type: Boolean, - default: true - }, - name: [String, Array], - src: [String, Array], - borderColor: [String], - ...baseProps - }, - data () { - return { - image: undefined, - hasLoaded: false - } - }, - created () { - // Should only invoke window.Image in the browser. - if (process.browser) { - this.loadImage(this.src) - } - }, - methods: { - loadImage (src) { - if (!canUseDOM) { - return - } - - const image = new window.Image() - image.src = src - - image.onload = event => { - this.hasLoaded = true - this.$emit('load', event) - } - - image.onError = event => { - this.hasLoaded = false - this.$emit('error', event) - } - } - }, - render (h) { - const avatarStyleProps = useAvatarStyles({ - size: this.size, - name: this.name, - showBorder: this.showBorder, - borderColor: this.borderColor, - theme: this.theme, - colorMode: this.colorMode - }) - - const theme = this.theme - const sizeKey = avatarSizes[this.size] - const _size = theme.sizes[sizeKey] - const fontSize = `calc(${_size} / 2.5)` - - /** - * @description Render child nodes for avatar - * @returns {Vue.VNode} - */ - const renderChildren = () => { - if (this.src && this.hasLoaded) { - return h(Box, { - props: { - as: 'img', - w: '100%', - h: '100%', - rounded: 'full', - objectFit: 'cover', - alt: this.name - }, - attrs: { - src: this.src - } - }) - } - - if (this.src && !this.hasLoaded) { - if (this.name) { - return h(AvatarName, { - props: { - name: this.name, - size: _size - } - }) - } else { - return h(DefaultAvatar, { - attrs: { - 'aria-label': this.name - } - }) - } - } - } - - return h(Box, { - props: { - fontSize: fontSize, - lineHeight: _size, - ...avatarStyleProps, - ...forwardProps(this.$props) - } - }, [ - renderChildren(), - this.$slots.default - ]) - } -} +export * from './Avatar' diff --git a/packages/chakra-ui-core/src/AvatarGroup/AvatarGroup.js b/packages/chakra-ui-core/src/AvatarGroup/AvatarGroup.js new file mode 100644 index 00000000..730670a2 --- /dev/null +++ b/packages/chakra-ui-core/src/AvatarGroup/AvatarGroup.js @@ -0,0 +1,108 @@ +import Flex from '../Flex' +import { avatarSizes } from '../Avatar/avatar.styles' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' + +/** + * For excess avatars we dispay this to show the remaining unrendered avatars + */ +const MoreAvatarLabel = { + name: 'MoreAvatarLabel', + inject: ['$theme', '$colorMode'], + props: { + size: [String, Array], + label: String, + ...baseProps + }, + computed: { + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + } + }, + render (h) { + const borderColor = { light: '#fff', dark: 'gray.800' } + const bg = { light: 'gray.200', dark: 'whiteAlpha.400' } + + const theme = this.theme + const sizeKey = avatarSizes[this.size] + const _size = theme.sizes[sizeKey] + const fontSize = `calc(${_size} / 2.75)` + + return h(Flex, { + props: { + w: avatarSizes[this.size], + h: avatarSizes[this.size], + bg: bg[this.colorMode], + color: 'inherit', + rounded: 'full', + alignItems: 'center', + justifyContent: 'center', + border: '2px', + borderColor: borderColor[this.colorMode], + fontSize: fontSize, + ...forwardProps(this.$props) + } + }, this.label) + } +} + +const AvatarGroup = { + name: 'AvatarGroup', + props: { + groupSize: { + type: [Number, String, Array], + default: 'md' + }, + borderColor: [String, Array], + max: [Number, String, Array], + spacing: { + type: [Number, String, Array], + default: -3 + }, + ...baseProps + }, + render (h) { + // Get the number of slot nodes inside AvatarGroup + const children = this.$slots.default.filter(e => e.tag) + const count = children.length + const max = parseInt(this.max, 10) + + // Apply styles to slot VNodes. + const clones = children.map((node, index) => { + const isFirstAvatar = index === 0 + if (!this.max || (max && index < max)) { + // Change VNode component options + const { propsData } = node.componentOptions + propsData['ml'] = isFirstAvatar ? 0 : this.spacing + propsData['size'] = this.groupSize + propsData['showBorder'] = true + propsData['borderColor'] = this.borderColor || propsData['borderColor'] + propsData['zIndex'] = count - index + return node + } + + if (max && index === max) { + return h(MoreAvatarLabel, { + props: { + size: this.groupSize, + ml: this.spacing, + label: `+${count - max}` + } + }) + } + }) + + return h(Flex, { + props: { + alignItems: 'center', + zIndex: 0, + ...forwardProps(this.$props) + } + }, clones) + } +} + +export default AvatarGroup diff --git a/packages/chakra-ui-core/src/AvatarGroup/index.js b/packages/chakra-ui-core/src/AvatarGroup/index.js index 730670a2..a21bfa8c 100644 --- a/packages/chakra-ui-core/src/AvatarGroup/index.js +++ b/packages/chakra-ui-core/src/AvatarGroup/index.js @@ -1,108 +1,2 @@ -import Flex from '../Flex' -import { avatarSizes } from '../Avatar/avatar.styles' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' - -/** - * For excess avatars we dispay this to show the remaining unrendered avatars - */ -const MoreAvatarLabel = { - name: 'MoreAvatarLabel', - inject: ['$theme', '$colorMode'], - props: { - size: [String, Array], - label: String, - ...baseProps - }, - computed: { - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - } - }, - render (h) { - const borderColor = { light: '#fff', dark: 'gray.800' } - const bg = { light: 'gray.200', dark: 'whiteAlpha.400' } - - const theme = this.theme - const sizeKey = avatarSizes[this.size] - const _size = theme.sizes[sizeKey] - const fontSize = `calc(${_size} / 2.75)` - - return h(Flex, { - props: { - w: avatarSizes[this.size], - h: avatarSizes[this.size], - bg: bg[this.colorMode], - color: 'inherit', - rounded: 'full', - alignItems: 'center', - justifyContent: 'center', - border: '2px', - borderColor: borderColor[this.colorMode], - fontSize: fontSize, - ...forwardProps(this.$props) - } - }, this.label) - } -} - -const AvatarGroup = { - name: 'AvatarGroup', - props: { - groupSize: { - type: [Number, String, Array], - default: 'md' - }, - borderColor: [String, Array], - max: [Number, String, Array], - spacing: { - type: [Number, String, Array], - default: -3 - }, - ...baseProps - }, - render (h) { - // Get the number of slot nodes inside AvatarGroup - const children = this.$slots.default.filter(e => e.tag) - const count = children.length - const max = parseInt(this.max, 10) - - // Apply styles to slot VNodes. - const clones = children.map((node, index) => { - const isFirstAvatar = index === 0 - if (!this.max || (max && index < max)) { - // Change VNode component options - const { propsData } = node.componentOptions - propsData['ml'] = isFirstAvatar ? 0 : this.spacing - propsData['size'] = this.groupSize - propsData['showBorder'] = true - propsData['borderColor'] = this.borderColor || propsData['borderColor'] - propsData['zIndex'] = count - index - return node - } - - if (max && index === max) { - return h(MoreAvatarLabel, { - props: { - size: this.groupSize, - ml: this.spacing, - label: `+${count - max}` - } - }) - } - }) - - return h(Flex, { - props: { - alignItems: 'center', - zIndex: 0, - ...forwardProps(this.$props) - } - }, clones) - } -} - +import AvatarGroup from './AvatarGroup' export default AvatarGroup diff --git a/packages/chakra-ui-core/src/Badge/Badge.js b/packages/chakra-ui-core/src/Badge/Badge.js new file mode 100644 index 00000000..ab9fabfe --- /dev/null +++ b/packages/chakra-ui-core/src/Badge/Badge.js @@ -0,0 +1,49 @@ +import Box from '../Box' +import { forwardProps } from '../utils' +import { baseProps } from '../config/props' +import useBadgeStyles from './badge.styles' + +export default { + name: 'Badge', + inject: ['$theme', '$colorMode'], + props: { + variant: { + type: String, + default: 'subtle' + }, + variantColor: { + type: String, + default: 'gray' + }, + ...baseProps + }, + computed: { + colorMode () { + return this.$colorMode() + } + }, + render (h) { + const badgeStyleProps = useBadgeStyles({ + theme: this.$theme(), + colorMode: this.colorMode, + color: this.variantColor, + variant: this.variant + }) + + return h(Box, { + props: { + ...forwardProps(this.$props), + d: 'inline-block', + textTransform: 'uppercase', + fontSize: 'xs', + fontFamily: 'body', + px: 1, + rounded: 'sm', + fontWeight: 'bold', + whiteSpace: 'nowrap', + verticalAlign: 'middle', + ...badgeStyleProps + } + }, this.$slots.default) + } +} diff --git a/packages/chakra-ui-core/src/Badge/index.js b/packages/chakra-ui-core/src/Badge/index.js index ab9fabfe..4391c3b0 100644 --- a/packages/chakra-ui-core/src/Badge/index.js +++ b/packages/chakra-ui-core/src/Badge/index.js @@ -1,49 +1,2 @@ -import Box from '../Box' -import { forwardProps } from '../utils' -import { baseProps } from '../config/props' -import useBadgeStyles from './badge.styles' - -export default { - name: 'Badge', - inject: ['$theme', '$colorMode'], - props: { - variant: { - type: String, - default: 'subtle' - }, - variantColor: { - type: String, - default: 'gray' - }, - ...baseProps - }, - computed: { - colorMode () { - return this.$colorMode() - } - }, - render (h) { - const badgeStyleProps = useBadgeStyles({ - theme: this.$theme(), - colorMode: this.colorMode, - color: this.variantColor, - variant: this.variant - }) - - return h(Box, { - props: { - ...forwardProps(this.$props), - d: 'inline-block', - textTransform: 'uppercase', - fontSize: 'xs', - fontFamily: 'body', - px: 1, - rounded: 'sm', - fontWeight: 'bold', - whiteSpace: 'nowrap', - verticalAlign: 'middle', - ...badgeStyleProps - } - }, this.$slots.default) - } -} +import Badge from './Badge' +export default Badge diff --git a/packages/chakra-ui-core/src/Box/Box.js b/packages/chakra-ui-core/src/Box/Box.js new file mode 100644 index 00000000..d960cb2b --- /dev/null +++ b/packages/chakra-ui-core/src/Box/Box.js @@ -0,0 +1,96 @@ + +import { css } from 'emotion' +import { background, border, color, borderRadius, flexbox, grid, layout, position, shadow, space, typography, compose } from 'styled-system' +import { baseProps, propsConfig } from '../config/props' +import { forwardProps } from '../utils' + +const baseEllipsis = { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' +} + +/** + * @description Truncates text if `truncate` is set to true. + * @param {Object} props Props + */ +const truncate = props => { + if (props.truncate) { + if (!props.lineClamp) { + return baseEllipsis + } + } +} + +/** + * @description Clamps text based on number of lines. + * @param {Object} props Props + */ +const clamp = props => { + if (props.lineClamp) { + return { + ...baseEllipsis, + '-webkit-box-orient': 'vertical', + '-webkit-line-clamp': `${props.lineClamp}` + } + } +} + +const decorate = props => { + if (props.textDecoration || props.textDecor) { + return { + 'text-decoration': `${props.textDecoration || props.textDecor}` + } + } +} + +export const systemProps = compose( + space, + layout, + color, + background, + border, + borderRadius, + grid, + position, + shadow, + decorate, + typography, + flexbox, + propsConfig, + truncate, + clamp +) + +const Box = { + name: 'Box', + inject: ['$theme'], + props: { + as: { + type: [String, Object], + default: 'div' + }, + to: { + type: [String, Object], + default: '' + }, + ...baseProps + }, + computed: { + theme () { + return this.$theme() + } + }, + render (h) { + const { as, to, ...cleanedStyleProps } = forwardProps(this.$props) + const boxStylesObject = systemProps({ ...cleanedStyleProps, theme: this.theme }) + + return h(as, { + props: { to }, + class: css(boxStylesObject), + on: this.$listeners + }, this.$slots.default) + } +} + +export default Box diff --git a/packages/chakra-ui-core/src/Box/index.js b/packages/chakra-ui-core/src/Box/index.js index d960cb2b..4a1371d3 100644 --- a/packages/chakra-ui-core/src/Box/index.js +++ b/packages/chakra-ui-core/src/Box/index.js @@ -1,96 +1,2 @@ - -import { css } from 'emotion' -import { background, border, color, borderRadius, flexbox, grid, layout, position, shadow, space, typography, compose } from 'styled-system' -import { baseProps, propsConfig } from '../config/props' -import { forwardProps } from '../utils' - -const baseEllipsis = { - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap' -} - -/** - * @description Truncates text if `truncate` is set to true. - * @param {Object} props Props - */ -const truncate = props => { - if (props.truncate) { - if (!props.lineClamp) { - return baseEllipsis - } - } -} - -/** - * @description Clamps text based on number of lines. - * @param {Object} props Props - */ -const clamp = props => { - if (props.lineClamp) { - return { - ...baseEllipsis, - '-webkit-box-orient': 'vertical', - '-webkit-line-clamp': `${props.lineClamp}` - } - } -} - -const decorate = props => { - if (props.textDecoration || props.textDecor) { - return { - 'text-decoration': `${props.textDecoration || props.textDecor}` - } - } -} - -export const systemProps = compose( - space, - layout, - color, - background, - border, - borderRadius, - grid, - position, - shadow, - decorate, - typography, - flexbox, - propsConfig, - truncate, - clamp -) - -const Box = { - name: 'Box', - inject: ['$theme'], - props: { - as: { - type: [String, Object], - default: 'div' - }, - to: { - type: [String, Object], - default: '' - }, - ...baseProps - }, - computed: { - theme () { - return this.$theme() - } - }, - render (h) { - const { as, to, ...cleanedStyleProps } = forwardProps(this.$props) - const boxStylesObject = systemProps({ ...cleanedStyleProps, theme: this.theme }) - - return h(as, { - props: { to }, - class: css(boxStylesObject), - on: this.$listeners - }, this.$slots.default) - } -} - +import Box from './Box' export default Box diff --git a/packages/chakra-ui-core/src/Breadcrumb/Breadcrumb.js b/packages/chakra-ui-core/src/Breadcrumb/Breadcrumb.js new file mode 100644 index 00000000..26acce14 --- /dev/null +++ b/packages/chakra-ui-core/src/Breadcrumb/Breadcrumb.js @@ -0,0 +1,169 @@ +import { baseProps } from '../config/props' +import Box from '../Box' +import Link from '../Link' +import { forwardProps, cloneVNodeElement, cleanChildren } from '../utils' + +const BreadcrumbSeparator = { + name: 'BreadcrumbSeparator', + props: { + ...baseProps, + spacing: [String, Number, Array], + separator: [String, Object] + }, + render (h) { + return h(Box, { + props: { + as: 'span', + mx: this.spacing, + ...forwardProps(this.$props) + }, + attrs: { + role: 'presentation' + } + }, [this.separator]) + } +} + +const Span = { + name: 'Span', + props: { + ...baseProps + }, + render (h) { + return h(Box, { + props: { + as: 'span', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const BreadcrumbLink = { + name: 'BreadcrumbLink', + props: { + ...baseProps, + isCurrentPage: Boolean + }, + render (h) { + const Comp = this.isCurrentPage ? Span : Link + + return h(Comp, { + props: forwardProps(this.$props), + attrs: { + 'aria-current': this.isCurrentPage ? 'page' : null + } + }, this.$slots.default) + } +} + +const BreadcrumbItem = { + name: 'BreadcrumbItem', + props: { + ...baseProps, + isCurrentPage: Boolean, + isLastChild: Boolean, + separator: [Object, String], + addSeparator: Boolean, + spacing: [String, Number, Array] + }, + render (h) { + const children = this.$slots.default.filter(e => e.tag) + const clones = children.map((vnode) => { + if (vnode.componentOptions.tag === BreadcrumbLink.name) { + const clone = cloneVNodeElement(vnode, { + props: { + isCurrentPage: this.isCurrentPage + } + }, h) + return clone + } + if (vnode.componentOptions.tag === BreadcrumbSeparator.name) { + const clone = cloneVNodeElement(vnode, { + props: { + spacing: this.spacing, + separator: this.separator + }, + children: vnode.componentOptions.children || this.separator + }, h) + return clone + } + }) + + return h(Box, { + props: { + display: 'inline-flex', + alignItems: 'center', + as: 'li' + } + }, [ + ...clones, + !this.isLastChild && this.addSeparator && h(BreadcrumbSeparator, { + props: { + spacing: this.spacing, + separator: this.separator + } + }) + ]) + } +} + +const Breadcrumb = { + name: 'Breadcrumb', + props: { + spacing: { + type: [String, Number, Array], + default: 2 + }, + addSeparator: { + type: Boolean, + default: true + }, + separator: { + type: [String, Object], + default: '/' + }, + ...baseProps + }, + render (h) { + const children = this.$slots.default + if (!children) { + console.error( + `[Chakra-ui:Breadcrumb]: Breadcrumb component should have at least one child` + ) + return null + } + const cleaned = cleanChildren(children) + const clones = cleaned.map((node, index, array) => { + return cloneVNodeElement(node, { + props: { + addSeparator: this.addSeparator, + separator: this.separator, + spacing: this.spacing, + isLastChild: array.length === index + 1 + } + }, h) + }) + + return h(Box, { + props: { + as: 'nav', + ...forwardProps(this.$props) + }, + attrs: { + 'aria-label': 'breadcrumb' + } + }, [h(Box, { + props: { + as: 'ol' + } + }, clones)]) + } +} + +export { + BreadcrumbSeparator, + BreadcrumbLink, + BreadcrumbItem, + Breadcrumb +} diff --git a/packages/chakra-ui-core/src/Breadcrumb/index.js b/packages/chakra-ui-core/src/Breadcrumb/index.js index 26acce14..e01e180c 100644 --- a/packages/chakra-ui-core/src/Breadcrumb/index.js +++ b/packages/chakra-ui-core/src/Breadcrumb/index.js @@ -1,169 +1 @@ -import { baseProps } from '../config/props' -import Box from '../Box' -import Link from '../Link' -import { forwardProps, cloneVNodeElement, cleanChildren } from '../utils' - -const BreadcrumbSeparator = { - name: 'BreadcrumbSeparator', - props: { - ...baseProps, - spacing: [String, Number, Array], - separator: [String, Object] - }, - render (h) { - return h(Box, { - props: { - as: 'span', - mx: this.spacing, - ...forwardProps(this.$props) - }, - attrs: { - role: 'presentation' - } - }, [this.separator]) - } -} - -const Span = { - name: 'Span', - props: { - ...baseProps - }, - render (h) { - return h(Box, { - props: { - as: 'span', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const BreadcrumbLink = { - name: 'BreadcrumbLink', - props: { - ...baseProps, - isCurrentPage: Boolean - }, - render (h) { - const Comp = this.isCurrentPage ? Span : Link - - return h(Comp, { - props: forwardProps(this.$props), - attrs: { - 'aria-current': this.isCurrentPage ? 'page' : null - } - }, this.$slots.default) - } -} - -const BreadcrumbItem = { - name: 'BreadcrumbItem', - props: { - ...baseProps, - isCurrentPage: Boolean, - isLastChild: Boolean, - separator: [Object, String], - addSeparator: Boolean, - spacing: [String, Number, Array] - }, - render (h) { - const children = this.$slots.default.filter(e => e.tag) - const clones = children.map((vnode) => { - if (vnode.componentOptions.tag === BreadcrumbLink.name) { - const clone = cloneVNodeElement(vnode, { - props: { - isCurrentPage: this.isCurrentPage - } - }, h) - return clone - } - if (vnode.componentOptions.tag === BreadcrumbSeparator.name) { - const clone = cloneVNodeElement(vnode, { - props: { - spacing: this.spacing, - separator: this.separator - }, - children: vnode.componentOptions.children || this.separator - }, h) - return clone - } - }) - - return h(Box, { - props: { - display: 'inline-flex', - alignItems: 'center', - as: 'li' - } - }, [ - ...clones, - !this.isLastChild && this.addSeparator && h(BreadcrumbSeparator, { - props: { - spacing: this.spacing, - separator: this.separator - } - }) - ]) - } -} - -const Breadcrumb = { - name: 'Breadcrumb', - props: { - spacing: { - type: [String, Number, Array], - default: 2 - }, - addSeparator: { - type: Boolean, - default: true - }, - separator: { - type: [String, Object], - default: '/' - }, - ...baseProps - }, - render (h) { - const children = this.$slots.default - if (!children) { - console.error( - `[Chakra-ui:Breadcrumb]: Breadcrumb component should have at least one child` - ) - return null - } - const cleaned = cleanChildren(children) - const clones = cleaned.map((node, index, array) => { - return cloneVNodeElement(node, { - props: { - addSeparator: this.addSeparator, - separator: this.separator, - spacing: this.spacing, - isLastChild: array.length === index + 1 - } - }, h) - }) - - return h(Box, { - props: { - as: 'nav', - ...forwardProps(this.$props) - }, - attrs: { - 'aria-label': 'breadcrumb' - } - }, [h(Box, { - props: { - as: 'ol' - } - }, clones)]) - } -} - -export { - BreadcrumbSeparator, - BreadcrumbLink, - BreadcrumbItem, - Breadcrumb -} +export * from './Breadcrumb' diff --git a/packages/chakra-ui-core/src/Button/Button.js b/packages/chakra-ui-core/src/Button/Button.js new file mode 100644 index 00000000..0d9b45f2 --- /dev/null +++ b/packages/chakra-ui-core/src/Button/Button.js @@ -0,0 +1,144 @@ +import styleProps from '../config/props' +import { buttonProps } from './button.props' +import { forwardProps } from '../utils' +import createButtonStyles, { setIconSizes } from './button.styles' +import Box from '../Box' +import PseudoBox from '../PseudoBox' +import Spinner from '../Spinner' +import Icon from '../Icon' + +/** + * Icon component in button. + */ +const ButtonIcon = { + name: 'ButtonIcon', + props: { + icon: { + type: [String, Object] + }, + size: { + type: [String, Number] + }, + ...styleProps + }, + render (h) { + if (typeof this.icon === 'string') { + return h(Icon, { + props: { + focusable: false, + name: this.icon, + color: 'currentColor', + ...setIconSizes(this.$props), + ...forwardProps(this.$props) + } + }) + } else { + return h(Box, { + props: { + as: this.icon, + focusable: false, + color: 'currentColor', + ...setIconSizes(this.$props), + ...forwardProps(this.$props) + }, + attrs: { + 'data-custom-icon': true + } + }) + } + } +} + +/** + * @description The Button component is an accessible rich component that does what a button does :) + */ +export default { + name: 'Button', + inject: ['$theme', '$colorMode'], + props: { + ...buttonProps, + ...styleProps, + to: [String, Object] + }, + computed: { + colorMode () { + return this.$colorMode() + }, + theme () { + return this.$theme() + } + }, + render (h) { + const buttonStyles = createButtonStyles({ + color: this.variantColor || this.cast, + variant: this.variant, + theme: this.theme, + ripple: this.ripple, + colorMode: this.colorMode, + size: this.size || 'md' + }) + + return h(PseudoBox, { + props: { + as: this.as, + to: this.to, + outline: 'none', + cursor: 'pointer', + fontSize: 'md', + fontWeight: '700', + border: 'none', + rounded: 'md', + width: this.isFullWidth ? 'full' : undefined, + ...buttonStyles, + ...forwardProps(this.$props) + }, + attrs: { + type: this.type, + tabIndex: 0, + disabled: this.isDisabled || this.isLoading, + 'aria-disabled': this.isDisabled || this.isLoading, + dataActive: this.isActive ? 'true' : undefined + }, + nativeOn: { + click: ($event) => this.$emit('click', $event) + } + }, [ + this.leftIcon && h(ButtonIcon, { + props: { + mr: this.iconSpacing, + mb: 'px', + icon: this.leftIcon, + size: '1em', + opacity: this.isLoading ? 0 : 1 + } + }), + this.isLoading && h(Spinner, { + props: { + position: this.loadingText ? 'relative' : 'absolute', + color: 'currentColor', + mb: '-4px', + mr: this.loadingText ? this.iconSpacing : 0, + size: '1em' + }, + attrs: { + 'chakra-button-spinner': '' + } + }), + this.isLoading ? this.loadingText || h(Box, { + props: { + as: 'span', + opacity: 0 + } + }, this.$slots.default) : this.$slots.default, + this.rightIcon && h(ButtonIcon, { + props: { + ml: this.iconSpacing, + mb: 'px', + icon: this.rightIcon, + size: '1em', + opacity: this.isLoading ? 0 : 1 + } + }) + ]) + } +} diff --git a/packages/chakra-ui-core/src/Button/index.js b/packages/chakra-ui-core/src/Button/index.js index 610e49dc..dd14732e 100644 --- a/packages/chakra-ui-core/src/Button/index.js +++ b/packages/chakra-ui-core/src/Button/index.js @@ -1,142 +1,2 @@ -import styleProps from '../config/props' -import { buttonProps } from './button.props' -import { forwardProps } from '../utils' -import createButtonStyles, { setIconSizes } from './button.styles' -import Box from '../Box' -import PseudoBox from '../PseudoBox' -import Spinner from '../Spinner' -import Icon from '../Icon' - -/** - * Icon component in button. - */ -const ButtonIcon = { - name: 'ButtonIcon', - props: { - icon: { - type: [String, Object] - }, - size: { - type: [String, Number] - }, - ...styleProps - }, - render (h) { - if (typeof this.icon === 'string') { - return h(Icon, { - props: { - focusable: false, - name: this.icon, - color: 'currentColor', - ...setIconSizes(this.$props), - ...forwardProps(this.$props) - } - }) - } else { - return h(Box, { - props: { - as: this.icon, - focusable: false, - color: 'currentColor', - ...setIconSizes(this.$props), - ...forwardProps(this.$props) - }, - attrs: { - 'data-custom-icon': true - } - }) - } - } -} - -/** - * @description The Button component is an accessible rich component that does what a button does :) - */ -export default { - name: 'Button', - inject: ['$theme', '$colorMode'], - props: { - ...buttonProps, - ...styleProps, - to: [String, Object] - }, - computed: { - colorMode () { - return this.$colorMode() - }, - theme () { - return this.$theme() - } - }, - render (h) { - const buttonStyles = createButtonStyles({ - color: this.variantColor || this.cast, - variant: this.variant, - theme: this.theme, - ripple: this.ripple, - colorMode: this.colorMode, - size: this.size || 'md' - }) - - return h(PseudoBox, { - props: { - as: this.as, - to: this.to, - outline: 'none', - cursor: 'pointer', - fontSize: 'md', - fontWeight: '700', - border: 'none', - rounded: 'md', - width: this.isFullWidth ? 'full' : undefined, - ...buttonStyles, - ...forwardProps(this.$props) - }, - attrs: { - type: this.type, - tabIndex: 0, - disabled: this.isDisabled || this.isLoading, - 'aria-disabled': this.isDisabled || this.isLoading, - dataActive: this.isActive ? 'true' : undefined - }, - nativeOn: { - click: ($event) => this.$emit('click', $event) - } - }, [ - this.leftIcon && !this.isLoading && h(ButtonIcon, { - props: { - mr: this.iconSpacing, - mb: 'px', - icon: this.leftIcon, - size: '1em' - } - }), - this.isLoading && h(Spinner, { - props: { - position: this.loadingText ? 'relative' : 'absolute', - color: 'currentColor', - mb: '-4px', - mr: this.loadingText ? this.iconSpacing : 0, - size: '1em' - }, - attrs: { - 'chakra-button-spinner': '' - } - }), - this.isLoading ? this.loadingText || h(Box, { - props: { - as: 'span', - opacity: 0 - } - }, this.$slots.default) : this.$slots.default, - this.rightIcon && !this.isLoading && h(ButtonIcon, { - props: { - ml: this.iconSpacing, - mb: 'px', - icon: this.rightIcon, - size: '1em' - } - }) - ]) - } -} +import Button from './Button' +export default Button diff --git a/packages/chakra-ui-core/src/Button/tests/__snapshots__/Button.test.js.snap b/packages/chakra-ui-core/src/Button/tests/__snapshots__/Button.test.js.snap index a6cbde61..9ae3cd64 100644 --- a/packages/chakra-ui-core/src/Button/tests/__snapshots__/Button.test.js.snap +++ b/packages/chakra-ui-core/src/Button/tests/__snapshots__/Button.test.js.snap @@ -8,7 +8,7 @@ exports[`should display button with left icon 1`] = ` type="button" > @@ -35,7 +35,7 @@ exports[`should display button with right icon 1`] = ` > Email diff --git a/packages/chakra-ui-core/src/ButtonGroup/ButtonGroup.js b/packages/chakra-ui-core/src/ButtonGroup/ButtonGroup.js new file mode 100644 index 00000000..d1b0ca39 --- /dev/null +++ b/packages/chakra-ui-core/src/ButtonGroup/ButtonGroup.js @@ -0,0 +1,54 @@ +import Box from '../Box' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' + +const ButtonGroup = { + name: 'ButtonGroup', + props: { + size: [String, Array], + variantColor: [String, Array], + variant: [String, Array], + isAttached: Boolean, + spacing: { + type: [Number, Array, String], + default: 2 + }, + ...baseProps + }, + render (h) { + const children = this.$slots.default.filter(e => e.tag) + const count = children.length + + const clones = children.map((node, index) => { + const isFirst = index === 0 + const isLast = index === count - 1 + const { propsData } = node.componentOptions + propsData['size'] = this.size || propsData['size'] + propsData['variantColor'] = propsData['variantColor'] || this.variantColor + propsData['variant'] = propsData['variant'] || this.variant + propsData['rounded'] = propsData['rounded'] || this.rounded + propsData['_focus'] = { boxShadow: 'outline', zIndex: 1 } + + // Radius adjustment + node.componentOptions.propsData = { + ...propsData, + ...(!isLast && !this.isAttached && { mr: this.spacing }), + ...(isFirst && this.isAttached && { roundedRight: 0 }), + ...(isLast && this.isAttached && { roundedLeft: 0 }), + ...(!isLast && this.isAttached && { borderRight: 0 }), + ...(!isFirst && !isLast && this.isAttached && { rounded: 0 }) + } + + return node + }) + + return h(Box, { + props: { + d: 'inline-block', + ...forwardProps(this.$props) + } + }, clones) + } +} + +export default ButtonGroup diff --git a/packages/chakra-ui-core/src/ButtonGroup/index.js b/packages/chakra-ui-core/src/ButtonGroup/index.js index d1b0ca39..f38e3ddf 100644 --- a/packages/chakra-ui-core/src/ButtonGroup/index.js +++ b/packages/chakra-ui-core/src/ButtonGroup/index.js @@ -1,54 +1,2 @@ -import Box from '../Box' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' - -const ButtonGroup = { - name: 'ButtonGroup', - props: { - size: [String, Array], - variantColor: [String, Array], - variant: [String, Array], - isAttached: Boolean, - spacing: { - type: [Number, Array, String], - default: 2 - }, - ...baseProps - }, - render (h) { - const children = this.$slots.default.filter(e => e.tag) - const count = children.length - - const clones = children.map((node, index) => { - const isFirst = index === 0 - const isLast = index === count - 1 - const { propsData } = node.componentOptions - propsData['size'] = this.size || propsData['size'] - propsData['variantColor'] = propsData['variantColor'] || this.variantColor - propsData['variant'] = propsData['variant'] || this.variant - propsData['rounded'] = propsData['rounded'] || this.rounded - propsData['_focus'] = { boxShadow: 'outline', zIndex: 1 } - - // Radius adjustment - node.componentOptions.propsData = { - ...propsData, - ...(!isLast && !this.isAttached && { mr: this.spacing }), - ...(isFirst && this.isAttached && { roundedRight: 0 }), - ...(isLast && this.isAttached && { roundedLeft: 0 }), - ...(!isLast && this.isAttached && { borderRight: 0 }), - ...(!isFirst && !isLast && this.isAttached && { rounded: 0 }) - } - - return node - }) - - return h(Box, { - props: { - d: 'inline-block', - ...forwardProps(this.$props) - } - }, clones) - } -} - +import ButtonGroup from './ButtonGroup' export default ButtonGroup diff --git a/packages/chakra-ui-core/src/CSSReset/CSSReset.js b/packages/chakra-ui-core/src/CSSReset/CSSReset.js new file mode 100644 index 00000000..5b49171a --- /dev/null +++ b/packages/chakra-ui-core/src/CSSReset/CSSReset.js @@ -0,0 +1,71 @@ +import { injectGlobal } from 'emotion' +import { useTailwindPreflight } from './preflight' + +const defaultConfig = theme => ({ + light: { + color: theme.colors.gray[800], + bg: undefined, + borderColor: theme.colors.gray[200], + placeholderColor: theme.colors.gray[400] + }, + dark: { + color: theme.colors.whiteAlpha[900], + bg: theme.colors.gray[800], + borderColor: theme.colors.whiteAlpha[300], + placeholderColor: theme.colors.whiteAlpha[400] + } +}) + +export default { + name: 'CSSReset', + inject: ['$theme', '$colorMode'], + computed: { + colorMode () { + return this.$colorMode() + }, + theme () { + return this.$theme() + }, + styleConfig () { + const _defaultConfig = defaultConfig(this.theme) + return this.config + ? this.config(this.theme, _defaultConfig) + : defaultConfig(this.theme) + } + }, + props: { + config: Object + }, + mounted () { + const { color, bg, borderColor, placeholderColor } = this.styleConfig[this.colorMode] + useTailwindPreflight(this.theme) + injectGlobal({ + 'html': { + lineHeight: 1.5, + color: color, + backgroundColor: bg + }, + + '*, *::before, *::after': { + borderWidth: 0, + borderStyle: 'solid', + borderColor: borderColor + }, + + 'input:-ms-input-placeholder, textarea:-ms-input-placeholder': { + color: placeholderColor + }, + + 'input::-ms-input-placeholder, textarea::-ms-input-placeholder': { + color: placeholderColor + }, + + 'input::placeholder, textarea::placeholder': { + color: placeholderColor + } + }) + }, + render () { + return null + } +} diff --git a/packages/chakra-ui-core/src/CSSReset/index.js b/packages/chakra-ui-core/src/CSSReset/index.js index 5b49171a..adf18391 100644 --- a/packages/chakra-ui-core/src/CSSReset/index.js +++ b/packages/chakra-ui-core/src/CSSReset/index.js @@ -1,71 +1,2 @@ -import { injectGlobal } from 'emotion' -import { useTailwindPreflight } from './preflight' - -const defaultConfig = theme => ({ - light: { - color: theme.colors.gray[800], - bg: undefined, - borderColor: theme.colors.gray[200], - placeholderColor: theme.colors.gray[400] - }, - dark: { - color: theme.colors.whiteAlpha[900], - bg: theme.colors.gray[800], - borderColor: theme.colors.whiteAlpha[300], - placeholderColor: theme.colors.whiteAlpha[400] - } -}) - -export default { - name: 'CSSReset', - inject: ['$theme', '$colorMode'], - computed: { - colorMode () { - return this.$colorMode() - }, - theme () { - return this.$theme() - }, - styleConfig () { - const _defaultConfig = defaultConfig(this.theme) - return this.config - ? this.config(this.theme, _defaultConfig) - : defaultConfig(this.theme) - } - }, - props: { - config: Object - }, - mounted () { - const { color, bg, borderColor, placeholderColor } = this.styleConfig[this.colorMode] - useTailwindPreflight(this.theme) - injectGlobal({ - 'html': { - lineHeight: 1.5, - color: color, - backgroundColor: bg - }, - - '*, *::before, *::after': { - borderWidth: 0, - borderStyle: 'solid', - borderColor: borderColor - }, - - 'input:-ms-input-placeholder, textarea:-ms-input-placeholder': { - color: placeholderColor - }, - - 'input::-ms-input-placeholder, textarea::-ms-input-placeholder': { - color: placeholderColor - }, - - 'input::placeholder, textarea::placeholder': { - color: placeholderColor - } - }) - }, - render () { - return null - } -} +import CSSReset from './CSSReset' +export default CSSReset diff --git a/packages/chakra-ui-core/src/Checkbox/Checkbox.js b/packages/chakra-ui-core/src/Checkbox/Checkbox.js new file mode 100644 index 00000000..b7471a1e --- /dev/null +++ b/packages/chakra-ui-core/src/Checkbox/Checkbox.js @@ -0,0 +1,150 @@ +import { StringNumber, StringArray } from '../config/props/props.types' +import { baseProps } from '../config' +import { useVariantColorWarning, useId } from '../utils' +import useCheckboxStyle from './checkbox.styles' +import Box from '../Box' +import VisuallyHidden from '../VisuallyHidden' +import ControlBox from '../ControlBox' +import Icon from '../Icon' + +const Checkbox = { + name: 'Checkbox', + inject: ['$theme', '$colorMode'], + model: { + prop: 'isChecked', + event: 'change' + }, + props: { + ...baseProps, + id: String, + name: String, + value: [String, Boolean], + ariaLabel: String, + ariaLabelledBy: String, + variantColor: { + type: String, + default: 'blue' + }, + defaultIsChecked: Boolean, + isChecked: { + type: Boolean, + default: false + }, + isFullWidth: Boolean, + size: { + type: String, + default: 'md' + }, + isDisabled: Boolean, + isInvalid: Boolean, + isReadOnly: Boolean, + isIndeterminate: Boolean, + iconColor: StringArray, + iconSize: { + type: StringNumber, + default: '10px' + } + }, + computed: { + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + }, + checkBoxStyles () { + return useCheckboxStyle({ + color: this.variantColor, + size: this.size, + colorMode: this.colorMode + }) + }, + _id () { + return this.id || `checkbox-${useId(4)}` + } + }, + created () { + // Ensure that the use of the variantColor props is consistent with theme. + useVariantColorWarning(this.theme, 'Checkbox', this.variantColor) + }, + methods: { + handleChange (e) { + this.$emit('change', !this.isChecked, e) + } + }, + render (h) { + const children = this.$slots.default + + return h(Box, { + props: { + ...this.$props, + as: 'label', + display: 'inline-flex', + verticalAlign: 'top', + alignItems: 'center', + width: this.isFullWidth ? 'full' : undefined, + cursor: this.isDisabled ? 'not-allowed' : 'pointer' + }, + attrs: { + for: this._id + } + }, [ + h(VisuallyHidden, { + props: { + as: 'input' + }, + domProps: { + value: this.value, + defaultChecked: this.isReadOnly ? undefined : this.defaultIsChecked, + checked: + this.isReadOnly + ? this.isChecked + : this.defaultIsChecked + ? undefined + : this.isChecked + }, + attrs: { + name: this.name, + type: 'checkbox', + id: this._id, + 'aria-label': this.ariaLabel, + 'aria-labelledby': this.ariaLabelledBy, + disabled: this.isDisabled, + readOnly: this.isReadOnly, + 'aria-readonly': this.isReadOnly, + 'aria-invalid': this.isInvalid, + 'aria-checked': this.isIndeterminate ? 'mixed' : this.isChecked + }, + nativeOn: { + change: this.isReadOnly ? undefined : this.handleChange + } + }), + h(ControlBox, { + props: { + opacity: this.isReadOnly ? 0.8 : 1, + ...this.checkBoxStyles + } + }, [ + h(Icon, { + props: { + name: this.isIndeterminate ? 'minus' : 'check', + size: this.iconSize, + color: this.iconColor, + transition: 'transform 240ms, opacity 240ms' + } + }) + ]), + children && h(Box, { + props: { + ml: 2, + fontSize: this.size, + fontFamily: 'body', + userSelect: 'none', + opacity: this.isDisabled ? 0.4 : 1 + } + }, children) + ]) + } +} + +export default Checkbox diff --git a/packages/chakra-ui-core/src/Checkbox/index.js b/packages/chakra-ui-core/src/Checkbox/index.js index b7471a1e..84f8d802 100644 --- a/packages/chakra-ui-core/src/Checkbox/index.js +++ b/packages/chakra-ui-core/src/Checkbox/index.js @@ -1,150 +1,2 @@ -import { StringNumber, StringArray } from '../config/props/props.types' -import { baseProps } from '../config' -import { useVariantColorWarning, useId } from '../utils' -import useCheckboxStyle from './checkbox.styles' -import Box from '../Box' -import VisuallyHidden from '../VisuallyHidden' -import ControlBox from '../ControlBox' -import Icon from '../Icon' - -const Checkbox = { - name: 'Checkbox', - inject: ['$theme', '$colorMode'], - model: { - prop: 'isChecked', - event: 'change' - }, - props: { - ...baseProps, - id: String, - name: String, - value: [String, Boolean], - ariaLabel: String, - ariaLabelledBy: String, - variantColor: { - type: String, - default: 'blue' - }, - defaultIsChecked: Boolean, - isChecked: { - type: Boolean, - default: false - }, - isFullWidth: Boolean, - size: { - type: String, - default: 'md' - }, - isDisabled: Boolean, - isInvalid: Boolean, - isReadOnly: Boolean, - isIndeterminate: Boolean, - iconColor: StringArray, - iconSize: { - type: StringNumber, - default: '10px' - } - }, - computed: { - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - }, - checkBoxStyles () { - return useCheckboxStyle({ - color: this.variantColor, - size: this.size, - colorMode: this.colorMode - }) - }, - _id () { - return this.id || `checkbox-${useId(4)}` - } - }, - created () { - // Ensure that the use of the variantColor props is consistent with theme. - useVariantColorWarning(this.theme, 'Checkbox', this.variantColor) - }, - methods: { - handleChange (e) { - this.$emit('change', !this.isChecked, e) - } - }, - render (h) { - const children = this.$slots.default - - return h(Box, { - props: { - ...this.$props, - as: 'label', - display: 'inline-flex', - verticalAlign: 'top', - alignItems: 'center', - width: this.isFullWidth ? 'full' : undefined, - cursor: this.isDisabled ? 'not-allowed' : 'pointer' - }, - attrs: { - for: this._id - } - }, [ - h(VisuallyHidden, { - props: { - as: 'input' - }, - domProps: { - value: this.value, - defaultChecked: this.isReadOnly ? undefined : this.defaultIsChecked, - checked: - this.isReadOnly - ? this.isChecked - : this.defaultIsChecked - ? undefined - : this.isChecked - }, - attrs: { - name: this.name, - type: 'checkbox', - id: this._id, - 'aria-label': this.ariaLabel, - 'aria-labelledby': this.ariaLabelledBy, - disabled: this.isDisabled, - readOnly: this.isReadOnly, - 'aria-readonly': this.isReadOnly, - 'aria-invalid': this.isInvalid, - 'aria-checked': this.isIndeterminate ? 'mixed' : this.isChecked - }, - nativeOn: { - change: this.isReadOnly ? undefined : this.handleChange - } - }), - h(ControlBox, { - props: { - opacity: this.isReadOnly ? 0.8 : 1, - ...this.checkBoxStyles - } - }, [ - h(Icon, { - props: { - name: this.isIndeterminate ? 'minus' : 'check', - size: this.iconSize, - color: this.iconColor, - transition: 'transform 240ms, opacity 240ms' - } - }) - ]), - children && h(Box, { - props: { - ml: 2, - fontSize: this.size, - fontFamily: 'body', - userSelect: 'none', - opacity: this.isDisabled ? 0.4 : 1 - } - }, children) - ]) - } -} - +import Checkbox from './Checkbox' export default Checkbox diff --git a/packages/chakra-ui-core/src/CheckboxGroup/CheckboxGroup.js b/packages/chakra-ui-core/src/CheckboxGroup/CheckboxGroup.js new file mode 100644 index 00000000..a9aaedfc --- /dev/null +++ b/packages/chakra-ui-core/src/CheckboxGroup/CheckboxGroup.js @@ -0,0 +1,96 @@ +import { SNA } from '../config/props/props.types' +import { baseProps } from '../config' +import Box from '../Box' +import { isDef, useId, cleanChildren, cloneVNodeElement, forwardProps } from '../utils' + +const CheckboxGroup = { + name: 'CheckboxGroup', + model: { + prop: 'value', + event: 'change' + }, + props: { + ...baseProps, + name: String, + variantColor: String, + size: String, + defaultValue: Array, + isInline: Boolean, + value: Array, + spacing: { + type: SNA, + default: 2 + } + }, + data () { + return { + values: this.defaultValue || [] + } + }, + computed: { + isControlled () { + return isDef(this.value) + }, + _values () { + return this.isControlled ? this.value : this.values + }, + _name () { + return this.name || `checkbox-group-${useId()}` + } + }, + methods: { + /** + * Handles change event for checkbox group + * @param {Event} event Event object + */ + onChange (val, event) { + const { checked, value } = event.target + let newValues + if (checked) { + newValues = [...this._values, value] + } else { + newValues = this._values.filter(val => val !== value) + } + + if (!this.isControlled) { + this.values = newValues + } + this.$emit('change', newValues) + } + }, + render (h) { + const children = cleanChildren(this.$slots.default) + const clones = children.map((vnode, index) => { + const isLastCheckbox = children.length === index + 1 + const spacingProps = this.isInline ? { mr: this.spacing } : { mb: this.spacing } + + const el = cloneVNodeElement(vnode, { + props: { + size: this.size, + variantColor: this.variantColor, + name: `${this.name}-${index}`, + isChecked: this._values.includes(vnode.componentOptions.propsData.value) + }, + on: { + change: this.onChange + } + }, h) + + return h(Box, { + props: { + display: this.isInline ? 'inline-block' : 'block', + ...(!isLastCheckbox && spacingProps) + } + }, [el]) + }) + + return h(Box, { + props: forwardProps(this.$props), + attrs: { + role: 'group' + } + }, clones) + } +} + +export default CheckboxGroup diff --git a/packages/chakra-ui-core/src/CheckboxGroup/index.js b/packages/chakra-ui-core/src/CheckboxGroup/index.js index a9aaedfc..e2c42b66 100644 --- a/packages/chakra-ui-core/src/CheckboxGroup/index.js +++ b/packages/chakra-ui-core/src/CheckboxGroup/index.js @@ -1,96 +1,2 @@ -import { SNA } from '../config/props/props.types' -import { baseProps } from '../config' -import Box from '../Box' -import { isDef, useId, cleanChildren, cloneVNodeElement, forwardProps } from '../utils' - -const CheckboxGroup = { - name: 'CheckboxGroup', - model: { - prop: 'value', - event: 'change' - }, - props: { - ...baseProps, - name: String, - variantColor: String, - size: String, - defaultValue: Array, - isInline: Boolean, - value: Array, - spacing: { - type: SNA, - default: 2 - } - }, - data () { - return { - values: this.defaultValue || [] - } - }, - computed: { - isControlled () { - return isDef(this.value) - }, - _values () { - return this.isControlled ? this.value : this.values - }, - _name () { - return this.name || `checkbox-group-${useId()}` - } - }, - methods: { - /** - * Handles change event for checkbox group - * @param {Event} event Event object - */ - onChange (val, event) { - const { checked, value } = event.target - let newValues - if (checked) { - newValues = [...this._values, value] - } else { - newValues = this._values.filter(val => val !== value) - } - - if (!this.isControlled) { - this.values = newValues - } - this.$emit('change', newValues) - } - }, - render (h) { - const children = cleanChildren(this.$slots.default) - const clones = children.map((vnode, index) => { - const isLastCheckbox = children.length === index + 1 - const spacingProps = this.isInline ? { mr: this.spacing } : { mb: this.spacing } - - const el = cloneVNodeElement(vnode, { - props: { - size: this.size, - variantColor: this.variantColor, - name: `${this.name}-${index}`, - isChecked: this._values.includes(vnode.componentOptions.propsData.value) - }, - on: { - change: this.onChange - } - }, h) - - return h(Box, { - props: { - display: this.isInline ? 'inline-block' : 'block', - ...(!isLastCheckbox && spacingProps) - } - }, [el]) - }) - - return h(Box, { - props: forwardProps(this.$props), - attrs: { - role: 'group' - } - }, clones) - } -} - +import CheckboxGroup from './CheckboxGroup' export default CheckboxGroup diff --git a/packages/chakra-ui-core/src/CircularProgress/CircularProgress.js b/packages/chakra-ui-core/src/CircularProgress/CircularProgress.js new file mode 100644 index 00000000..6f3189b6 --- /dev/null +++ b/packages/chakra-ui-core/src/CircularProgress/CircularProgress.js @@ -0,0 +1,118 @@ +import Box from '../Box' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' +import { getComputedProps } from './circularprogress.styles' + +const CircularProgressLabel = { + name: 'CircularProgressLabel', + props: baseProps, + render (h) { + return h(Box, { + style: { + fontVariantNumeric: 'tabular-nums' + }, + props: { + position: 'absolute', + left: '50%', + top: '50%', + lineHeight: '1', + transform: 'translate(-50%, -50%)', + fontSize: '0.25em', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const CircularProgress = { + name: 'CircularProgress', + inject: ['$colorMode'], + computed: { + colorMode () { + return this.$colorMode() + } + }, + props: { + size: { + type: String, + default: '48px' + }, + max: { + type: Number, + default: 100 + }, + min: { + typs: Number, + default: 0 + }, + isIndeterminate: Boolean, + isTransitioned: { + type: Boolean, + default: true + }, + thickness: { + type: Number, + default: 0.2 + }, + value: Number, + angle: { + type: Number, + default: 0 + }, + capIsRound: Boolean, + trackColor: { + type: String, + default: 'gray' + }, + color: { + type: String, + default: 'blue' + }, + ...baseProps + }, + render (h) { + const _trackColor = { light: `${this.trackColor}.100`, dark: 'whiteAlpha.300' } + const _color = { light: `${this.color}.500`, dark: `${this.color}.200` } + + const { + rootData, + indicatorCircleData, + svgData, + trackCircleData + } = getComputedProps({ + min: this.min, + max: this.max, + value: this.value, + size: this.size, + angle: this.angle, + thickness: this.thickness, + capIsRound: this.capIsRound, + isIndeterminate: this.isIndeterminate, + color: _color[this.colorMode], + trackColor: _trackColor[this.colorMode], + isTransitioned: this.isTransitioned + }) + + return h(Box, { + props: { + ...rootData.props, + ...forwardProps(this.$props) + }, + attrs: rootData.attrs + }, [ + h(Box, { + props: svgData.props, + attrs: svgData.attrs + }, [ + h(Box, { props: trackCircleData.props, attrs: trackCircleData.attrs }), + h(Box, { props: indicatorCircleData.props, attrs: indicatorCircleData.attrs }) + ]), + this.$slots.default + ]) + } +} + +export { + CircularProgress, + CircularProgressLabel +} diff --git a/packages/chakra-ui-core/src/CircularProgress/index.js b/packages/chakra-ui-core/src/CircularProgress/index.js index 6f3189b6..c045aa75 100644 --- a/packages/chakra-ui-core/src/CircularProgress/index.js +++ b/packages/chakra-ui-core/src/CircularProgress/index.js @@ -1,118 +1 @@ -import Box from '../Box' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' -import { getComputedProps } from './circularprogress.styles' - -const CircularProgressLabel = { - name: 'CircularProgressLabel', - props: baseProps, - render (h) { - return h(Box, { - style: { - fontVariantNumeric: 'tabular-nums' - }, - props: { - position: 'absolute', - left: '50%', - top: '50%', - lineHeight: '1', - transform: 'translate(-50%, -50%)', - fontSize: '0.25em', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const CircularProgress = { - name: 'CircularProgress', - inject: ['$colorMode'], - computed: { - colorMode () { - return this.$colorMode() - } - }, - props: { - size: { - type: String, - default: '48px' - }, - max: { - type: Number, - default: 100 - }, - min: { - typs: Number, - default: 0 - }, - isIndeterminate: Boolean, - isTransitioned: { - type: Boolean, - default: true - }, - thickness: { - type: Number, - default: 0.2 - }, - value: Number, - angle: { - type: Number, - default: 0 - }, - capIsRound: Boolean, - trackColor: { - type: String, - default: 'gray' - }, - color: { - type: String, - default: 'blue' - }, - ...baseProps - }, - render (h) { - const _trackColor = { light: `${this.trackColor}.100`, dark: 'whiteAlpha.300' } - const _color = { light: `${this.color}.500`, dark: `${this.color}.200` } - - const { - rootData, - indicatorCircleData, - svgData, - trackCircleData - } = getComputedProps({ - min: this.min, - max: this.max, - value: this.value, - size: this.size, - angle: this.angle, - thickness: this.thickness, - capIsRound: this.capIsRound, - isIndeterminate: this.isIndeterminate, - color: _color[this.colorMode], - trackColor: _trackColor[this.colorMode], - isTransitioned: this.isTransitioned - }) - - return h(Box, { - props: { - ...rootData.props, - ...forwardProps(this.$props) - }, - attrs: rootData.attrs - }, [ - h(Box, { - props: svgData.props, - attrs: svgData.attrs - }, [ - h(Box, { props: trackCircleData.props, attrs: trackCircleData.attrs }), - h(Box, { props: indicatorCircleData.props, attrs: indicatorCircleData.attrs }) - ]), - this.$slots.default - ]) - } -} - -export { - CircularProgress, - CircularProgressLabel -} +export * from './CircularProgress' diff --git a/packages/chakra-ui-core/src/ClickOutside/ClickOutside.js b/packages/chakra-ui-core/src/ClickOutside/ClickOutside.js new file mode 100644 index 00000000..c514359d --- /dev/null +++ b/packages/chakra-ui-core/src/ClickOutside/ClickOutside.js @@ -0,0 +1,33 @@ +import { canUseDOM } from '../utils' + +const ClickOutside = { + name: 'ClickOutside', + props: { + whitelist: Array, + do: Function, + isDisabled: Boolean + }, + created () { + if (!this.isDisabled) { + const listener = (e, el) => { + if ( + e.target === el || + el.contains(e.target) || + (this.whitelist.includes(e.target)) + ) return + if (this.do) this.do() + } + + canUseDOM && document.addEventListener('click', (e) => listener(e, this.$el)) + + this.$once('hook:beforeDestroy', () => { + document.removeEventListener('click', (e) => listener(e, this.$el)) + }) + } + }, + render () { + return this.$slots.default[0] + } +} + +export default ClickOutside diff --git a/packages/chakra-ui-core/src/ClickOutside/index.js b/packages/chakra-ui-core/src/ClickOutside/index.js index c514359d..ac153f96 100644 --- a/packages/chakra-ui-core/src/ClickOutside/index.js +++ b/packages/chakra-ui-core/src/ClickOutside/index.js @@ -1,33 +1,2 @@ -import { canUseDOM } from '../utils' - -const ClickOutside = { - name: 'ClickOutside', - props: { - whitelist: Array, - do: Function, - isDisabled: Boolean - }, - created () { - if (!this.isDisabled) { - const listener = (e, el) => { - if ( - e.target === el || - el.contains(e.target) || - (this.whitelist.includes(e.target)) - ) return - if (this.do) this.do() - } - - canUseDOM && document.addEventListener('click', (e) => listener(e, this.$el)) - - this.$once('hook:beforeDestroy', () => { - document.removeEventListener('click', (e) => listener(e, this.$el)) - }) - } - }, - render () { - return this.$slots.default[0] - } -} - +import ClickOutside from './ClickOutside' export default ClickOutside diff --git a/packages/chakra-ui-core/src/CloseButton/CloseButton.js b/packages/chakra-ui-core/src/CloseButton/CloseButton.js new file mode 100644 index 00000000..95fa8474 --- /dev/null +++ b/packages/chakra-ui-core/src/CloseButton/CloseButton.js @@ -0,0 +1,106 @@ +import Icon from '../Icon' +import PseudoBox from '../PseudoBox' +import styleProps from '../config/props' +import { forwardProps } from '../utils' + +const baseProps = { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + rounded: 'md', + transition: 'all 0.2s', + flex: '0 0 auto', + _hover: { bg: 'blackAlpha.100' }, + _active: { bg: 'blackAlpha.200' }, + _disabled: { + cursor: 'not-allowed' + }, + _focus: { + boxShadow: 'outline' + }, + border: 'none', + bg: 'blackAlpha.50' +} + +const sizes = { + lg: { + button: '40px', + icon: '16px' + }, + md: { + button: '32px', + icon: '12px' + }, + sm: { + button: '24px', + icon: '10px' + } +} + +export default { + name: 'CloseButton', + inject: ['$theme', '$colorMode'], + props: { + size: { + type: String, + default: 'md', + validator: value => value.match(/^(sm|md|lg)$/) + }, + isDisabled: { + type: Boolean, + default: false + }, + color: { + type: String, + default: 'currentColor' + }, + _ariaLabel: { + type: String, + default: 'Close' + }, + ...styleProps + }, + render (h) { + // Pseudo styles + const hoverColor = { light: 'blackAlpha.100', dark: 'whiteAlpha.100' } + const activeColor = { light: 'blackAlpha.200', dark: 'whiteAlpha.200' } + + // Size styles + const buttonSize = sizes[this.size] && sizes[this.size]['button'] + const iconSize = sizes[this.size] && sizes[this.size]['icon'] + + return h(PseudoBox, { + props: { + as: 'button', + outline: 'none', + h: buttonSize, + w: buttonSize, + disabled: this.isDisabled, + cursor: 'pointer', + _hover: { bg: hoverColor[this.colorMode] }, + _active: { bg: activeColor[this.colorMode] }, + ...baseProps, + ...forwardProps(this.$props) + }, + nativeOn: { + click: ($e) => { + this.$emit('click', $e) + } + }, + attrs: { + 'aria-label': this._ariaLabel, + 'aria-disabled': this.isDisabled + } + }, [h(Icon, { + props: { + color: this.color, + name: 'close', + size: iconSize + }, + attrs: { + focusable: false, + 'aria-hidden': true + } + })]) + } +} diff --git a/packages/chakra-ui-core/src/CloseButton/index.js b/packages/chakra-ui-core/src/CloseButton/index.js index 95fa8474..bafb091f 100644 --- a/packages/chakra-ui-core/src/CloseButton/index.js +++ b/packages/chakra-ui-core/src/CloseButton/index.js @@ -1,106 +1,2 @@ -import Icon from '../Icon' -import PseudoBox from '../PseudoBox' -import styleProps from '../config/props' -import { forwardProps } from '../utils' - -const baseProps = { - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - rounded: 'md', - transition: 'all 0.2s', - flex: '0 0 auto', - _hover: { bg: 'blackAlpha.100' }, - _active: { bg: 'blackAlpha.200' }, - _disabled: { - cursor: 'not-allowed' - }, - _focus: { - boxShadow: 'outline' - }, - border: 'none', - bg: 'blackAlpha.50' -} - -const sizes = { - lg: { - button: '40px', - icon: '16px' - }, - md: { - button: '32px', - icon: '12px' - }, - sm: { - button: '24px', - icon: '10px' - } -} - -export default { - name: 'CloseButton', - inject: ['$theme', '$colorMode'], - props: { - size: { - type: String, - default: 'md', - validator: value => value.match(/^(sm|md|lg)$/) - }, - isDisabled: { - type: Boolean, - default: false - }, - color: { - type: String, - default: 'currentColor' - }, - _ariaLabel: { - type: String, - default: 'Close' - }, - ...styleProps - }, - render (h) { - // Pseudo styles - const hoverColor = { light: 'blackAlpha.100', dark: 'whiteAlpha.100' } - const activeColor = { light: 'blackAlpha.200', dark: 'whiteAlpha.200' } - - // Size styles - const buttonSize = sizes[this.size] && sizes[this.size]['button'] - const iconSize = sizes[this.size] && sizes[this.size]['icon'] - - return h(PseudoBox, { - props: { - as: 'button', - outline: 'none', - h: buttonSize, - w: buttonSize, - disabled: this.isDisabled, - cursor: 'pointer', - _hover: { bg: hoverColor[this.colorMode] }, - _active: { bg: activeColor[this.colorMode] }, - ...baseProps, - ...forwardProps(this.$props) - }, - nativeOn: { - click: ($e) => { - this.$emit('click', $e) - } - }, - attrs: { - 'aria-label': this._ariaLabel, - 'aria-disabled': this.isDisabled - } - }, [h(Icon, { - props: { - color: this.color, - name: 'close', - size: iconSize - }, - attrs: { - focusable: false, - 'aria-hidden': true - } - })]) - } -} +import CloseButton from './CloseButton' +export default CloseButton diff --git a/packages/chakra-ui-core/src/Code/Code.js b/packages/chakra-ui-core/src/Code/Code.js new file mode 100644 index 00000000..3e47c346 --- /dev/null +++ b/packages/chakra-ui-core/src/Code/Code.js @@ -0,0 +1,49 @@ +import Box from '../Box' +import useBadgeStyle from '../Badge/badge.styles' +import { useVariantColorWarning, forwardProps } from '../utils' +import { baseProps } from '../config/props' + +const Code = { + name: 'Code', + inject: ['$theme', '$colorMode'], + props: { + variantColor: { + type: String, + default: 'gray' + }, + ...baseProps + }, + computed: { + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + }, + badgeStyle () { + useVariantColorWarning(this.theme, 'Code', this.variantColor) + return useBadgeStyle({ + variant: 'subtle', + color: this.variantColor, + colorMode: this.colorMode, + theme: this.theme + }) + } + }, + render (h) { + return h(Box, { + props: { + as: 'code', + display: 'inline-block', + fontFamily: 'mono', + fontSize: 'sm', + px: '0.2em', + rounded: 'sm', + ...this.badgeStyle, + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +export default Code diff --git a/packages/chakra-ui-core/src/Code/index.js b/packages/chakra-ui-core/src/Code/index.js index 3e47c346..652c2a56 100644 --- a/packages/chakra-ui-core/src/Code/index.js +++ b/packages/chakra-ui-core/src/Code/index.js @@ -1,49 +1,2 @@ -import Box from '../Box' -import useBadgeStyle from '../Badge/badge.styles' -import { useVariantColorWarning, forwardProps } from '../utils' -import { baseProps } from '../config/props' - -const Code = { - name: 'Code', - inject: ['$theme', '$colorMode'], - props: { - variantColor: { - type: String, - default: 'gray' - }, - ...baseProps - }, - computed: { - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - }, - badgeStyle () { - useVariantColorWarning(this.theme, 'Code', this.variantColor) - return useBadgeStyle({ - variant: 'subtle', - color: this.variantColor, - colorMode: this.colorMode, - theme: this.theme - }) - } - }, - render (h) { - return h(Box, { - props: { - as: 'code', - display: 'inline-block', - fontFamily: 'mono', - fontSize: 'sm', - px: '0.2em', - rounded: 'sm', - ...this.badgeStyle, - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - +import Code from './Code' export default Code diff --git a/packages/chakra-ui-core/src/Collapse/Collapse.js b/packages/chakra-ui-core/src/Collapse/Collapse.js new file mode 100644 index 00000000..52943c5b --- /dev/null +++ b/packages/chakra-ui-core/src/Collapse/Collapse.js @@ -0,0 +1,50 @@ +import { AnimateHeight } from '../Transition' +import Box from '../Box' +import { forwardProps } from '../utils' + +const Collapse = { + name: 'Collapse', + props: { + isOpen: Boolean, + duration: { + type: Number, + default: 250 + }, + easing: { + type: String, + default: 'easeInOutSine' + }, + startingHeight: Number, + endingHeight: Number, + animateOpacity: { + type: Boolean, + default: true + } + }, + render (h) { + const children = this.$slots.default + + return h(AnimateHeight, { + props: { + isOpen: this.isOpen, + duration: this.duration, + enterEasing: this.easing, + leaveEasing: this.easing, + initialHeight: this.startingHeight, + finalHeight: this.endingHeight, + animateOpacity: this.animateOpacity + }, + on: { + enter: (e) => this.$emit('start', e), + leave: (e) => this.$emit('finish', e) + } + }, [h(Box, { + props: { + ...forwardProps(this.$props), + overflow: 'hidden' + } + }, children)]) + } +} + +export default Collapse diff --git a/packages/chakra-ui-core/src/Collapse/index.js b/packages/chakra-ui-core/src/Collapse/index.js index 52943c5b..49aedffd 100644 --- a/packages/chakra-ui-core/src/Collapse/index.js +++ b/packages/chakra-ui-core/src/Collapse/index.js @@ -1,50 +1,2 @@ -import { AnimateHeight } from '../Transition' -import Box from '../Box' -import { forwardProps } from '../utils' - -const Collapse = { - name: 'Collapse', - props: { - isOpen: Boolean, - duration: { - type: Number, - default: 250 - }, - easing: { - type: String, - default: 'easeInOutSine' - }, - startingHeight: Number, - endingHeight: Number, - animateOpacity: { - type: Boolean, - default: true - } - }, - render (h) { - const children = this.$slots.default - - return h(AnimateHeight, { - props: { - isOpen: this.isOpen, - duration: this.duration, - enterEasing: this.easing, - leaveEasing: this.easing, - initialHeight: this.startingHeight, - finalHeight: this.endingHeight, - animateOpacity: this.animateOpacity - }, - on: { - enter: (e) => this.$emit('start', e), - leave: (e) => this.$emit('finish', e) - } - }, [h(Box, { - props: { - ...forwardProps(this.$props), - overflow: 'hidden' - } - }, children)]) - } -} - +import Collapse from './Collapse' export default Collapse diff --git a/packages/chakra-ui-core/src/ColorModeProvider/ColorModeProvider.js b/packages/chakra-ui-core/src/ColorModeProvider/ColorModeProvider.js new file mode 100644 index 00000000..d6d52473 --- /dev/null +++ b/packages/chakra-ui-core/src/ColorModeProvider/ColorModeProvider.js @@ -0,0 +1,77 @@ +import { colorModeObserver } from '../utils/color-mode-observer' + +const ColorModeProvider = { + name: 'ColorModeProvider', + props: { + value: String + }, + data () { + return { + colorMode: 'light' + } + }, + provide () { + return { + $colorMode: () => this._colorMode, + $toggleColorMode: this.toggleColorMode + } + }, + computed: { + _colorMode: { + get () { + return this.value ? this.value : this.colorMode + }, + set (value) { + this.colorMode = value + } + } + }, + watch: { + _colorMode: { + immediate: true, + handler (newVal) { + colorModeObserver.colorMode = newVal + } + } + }, + methods: { + toggleColorMode () { + this._colorMode = this._colorMode === 'light' ? 'dark' : 'light' + } + }, + render () { + return this.$scopedSlots.default({ + colorMode: this._colorMode, + toggleColorMode: this.toggleColorMode + }) + } +} + +const DarkMode = { + name: 'DarkMode', + render (h) { + return h(ColorModeProvider, { + props: { + value: 'dark' + } + }, this.$slots.default) + } +} + +const LightMode = { + name: 'LightMode', + render (h) { + return h(ColorModeProvider, { + props: { + value: 'light' + } + }, this.$slots.default) + } +} + +export default ColorModeProvider + +export { + DarkMode, + LightMode +} diff --git a/packages/chakra-ui-core/src/ColorModeProvider/index.js b/packages/chakra-ui-core/src/ColorModeProvider/index.js index d6d52473..e2c4e639 100644 --- a/packages/chakra-ui-core/src/ColorModeProvider/index.js +++ b/packages/chakra-ui-core/src/ColorModeProvider/index.js @@ -1,77 +1,3 @@ -import { colorModeObserver } from '../utils/color-mode-observer' - -const ColorModeProvider = { - name: 'ColorModeProvider', - props: { - value: String - }, - data () { - return { - colorMode: 'light' - } - }, - provide () { - return { - $colorMode: () => this._colorMode, - $toggleColorMode: this.toggleColorMode - } - }, - computed: { - _colorMode: { - get () { - return this.value ? this.value : this.colorMode - }, - set (value) { - this.colorMode = value - } - } - }, - watch: { - _colorMode: { - immediate: true, - handler (newVal) { - colorModeObserver.colorMode = newVal - } - } - }, - methods: { - toggleColorMode () { - this._colorMode = this._colorMode === 'light' ? 'dark' : 'light' - } - }, - render () { - return this.$scopedSlots.default({ - colorMode: this._colorMode, - toggleColorMode: this.toggleColorMode - }) - } -} - -const DarkMode = { - name: 'DarkMode', - render (h) { - return h(ColorModeProvider, { - props: { - value: 'dark' - } - }, this.$slots.default) - } -} - -const LightMode = { - name: 'LightMode', - render (h) { - return h(ColorModeProvider, { - props: { - value: 'light' - } - }, this.$slots.default) - } -} - +import ColorModeProvider from './ColorModeProvider' export default ColorModeProvider - -export { - DarkMode, - LightMode -} +export * from './ColorModeProvider' diff --git a/packages/chakra-ui-core/src/ControlBox/ControlBox.js b/packages/chakra-ui-core/src/ControlBox/ControlBox.js new file mode 100644 index 00000000..435bfd9a --- /dev/null +++ b/packages/chakra-ui-core/src/ControlBox/ControlBox.js @@ -0,0 +1,91 @@ +import { css } from 'emotion' +import __css from '@styled-system/css' +import Box from '../Box' +import { tx, forwardProps } from '../utils' +import { baseProps } from '../config' + +// Default ControlBox props types +const PropTypes = [Object, Array] + +const ControlBox = { + name: 'ControlBox', + inject: ['$theme'], + props: { + type: { + type: String, + default: 'checkbox' + }, + size: { + type: [Number, String, Array], + default: 'auto' + }, + _hover: PropTypes, + _invalid: PropTypes, + _disabled: PropTypes, + _focus: PropTypes, + _checked: PropTypes, + _child: { + type: PropTypes, + default: () => ({ opacity: 0 }) + }, + _checkedAndChild: { + type: PropTypes, + default: () => ({ opacity: 1 }) + }, + _checkedAndDisabled: PropTypes, + _checkedAndFocus: PropTypes, + _checkedAndHover: PropTypes, + ...baseProps + }, + computed: { + theme () { + return this.$theme() + }, + className () { + const checkedAndDisabled = `input[type=${this.type}]:checked:disabled + &, input[type=${this.type}][aria-checked=mixed]:disabled + &` + const checkedAndHover = `input[type=${this.type}]:checked:hover:not(:disabled) + &, input[type=${this.type}][aria-checked=mixed]:hover:not(:disabled) + &` + const checkedAndFocus = `input[type=${this.type}]:checked:focus + &, input[type=${this.type}][aria-checked=mixed]:focus + &` + const disabled = `input[type=${this.type}]:disabled + &` + const focus = `input[type=${this.type}]:focus + &` + const hover = `input[type=${this.type}]:hover:not(:disabled):not(:checked) + &` + const checked = `input[type=${this.type}]:checked + &, input[type=${this.type}][aria-checked=mixed] + &` + const invalid = `input[type=${this.type}][aria-invalid=true] + &` + + const controlBoxStyleObject = __css({ + [focus]: tx(this._focus), + [hover]: tx(this._hover), + [disabled]: tx(this._disabled), + [invalid]: tx(this._invalid), + [checkedAndDisabled]: tx(this._checkedAndDisabled), + [checkedAndFocus]: tx(this._checkedAndFocus), + [checkedAndHover]: tx(this._checkedAndHover), + '& > *': tx(this._child), + [checked]: { + ...tx(this._checked), + '& > *': tx(this._checkedAndChild) + } + })(this.theme) + return css(controlBoxStyleObject) + } + }, + render (h) { + return h(Box, { + class: [this.className], + props: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 120ms', + flexShrink: '0', + width: this.size, + height: this.size, + ...forwardProps(this.$props) + }, + attrs: { + 'aria-hidden': 'true' + } + }, this.$slots.default) + } +} + +export default ControlBox diff --git a/packages/chakra-ui-core/src/ControlBox/index.js b/packages/chakra-ui-core/src/ControlBox/index.js index 435bfd9a..a1e2c6cf 100644 --- a/packages/chakra-ui-core/src/ControlBox/index.js +++ b/packages/chakra-ui-core/src/ControlBox/index.js @@ -1,91 +1,2 @@ -import { css } from 'emotion' -import __css from '@styled-system/css' -import Box from '../Box' -import { tx, forwardProps } from '../utils' -import { baseProps } from '../config' - -// Default ControlBox props types -const PropTypes = [Object, Array] - -const ControlBox = { - name: 'ControlBox', - inject: ['$theme'], - props: { - type: { - type: String, - default: 'checkbox' - }, - size: { - type: [Number, String, Array], - default: 'auto' - }, - _hover: PropTypes, - _invalid: PropTypes, - _disabled: PropTypes, - _focus: PropTypes, - _checked: PropTypes, - _child: { - type: PropTypes, - default: () => ({ opacity: 0 }) - }, - _checkedAndChild: { - type: PropTypes, - default: () => ({ opacity: 1 }) - }, - _checkedAndDisabled: PropTypes, - _checkedAndFocus: PropTypes, - _checkedAndHover: PropTypes, - ...baseProps - }, - computed: { - theme () { - return this.$theme() - }, - className () { - const checkedAndDisabled = `input[type=${this.type}]:checked:disabled + &, input[type=${this.type}][aria-checked=mixed]:disabled + &` - const checkedAndHover = `input[type=${this.type}]:checked:hover:not(:disabled) + &, input[type=${this.type}][aria-checked=mixed]:hover:not(:disabled) + &` - const checkedAndFocus = `input[type=${this.type}]:checked:focus + &, input[type=${this.type}][aria-checked=mixed]:focus + &` - const disabled = `input[type=${this.type}]:disabled + &` - const focus = `input[type=${this.type}]:focus + &` - const hover = `input[type=${this.type}]:hover:not(:disabled):not(:checked) + &` - const checked = `input[type=${this.type}]:checked + &, input[type=${this.type}][aria-checked=mixed] + &` - const invalid = `input[type=${this.type}][aria-invalid=true] + &` - - const controlBoxStyleObject = __css({ - [focus]: tx(this._focus), - [hover]: tx(this._hover), - [disabled]: tx(this._disabled), - [invalid]: tx(this._invalid), - [checkedAndDisabled]: tx(this._checkedAndDisabled), - [checkedAndFocus]: tx(this._checkedAndFocus), - [checkedAndHover]: tx(this._checkedAndHover), - '& > *': tx(this._child), - [checked]: { - ...tx(this._checked), - '& > *': tx(this._checkedAndChild) - } - })(this.theme) - return css(controlBoxStyleObject) - } - }, - render (h) { - return h(Box, { - class: [this.className], - props: { - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - transition: 'all 120ms', - flexShrink: '0', - width: this.size, - height: this.size, - ...forwardProps(this.$props) - }, - attrs: { - 'aria-hidden': 'true' - } - }, this.$slots.default) - } -} - +import ControlBox from './ControlBox' export default ControlBox diff --git a/packages/chakra-ui-core/src/Css/Css.js b/packages/chakra-ui-core/src/Css/Css.js new file mode 100644 index 00000000..f9224a94 --- /dev/null +++ b/packages/chakra-ui-core/src/Css/Css.js @@ -0,0 +1,5 @@ +import _css from '@styled-system/css' +import { tx } from '../utils' + +const Css = styleProps => _css(tx(styleProps)) +export default Css diff --git a/packages/chakra-ui-core/src/Css/index.js b/packages/chakra-ui-core/src/Css/index.js index 51b2f0d4..4ffab1a9 100644 --- a/packages/chakra-ui-core/src/Css/index.js +++ b/packages/chakra-ui-core/src/Css/index.js @@ -1,5 +1,2 @@ -import _css from '@styled-system/css' -import { tx } from '../utils' - -const css = styleProps => _css(tx(styleProps)) -export default css +import Css from './Css' +export default Css diff --git a/packages/chakra-ui-core/src/Divider/Divider.js b/packages/chakra-ui-core/src/Divider/Divider.js new file mode 100644 index 00000000..1e4f4130 --- /dev/null +++ b/packages/chakra-ui-core/src/Divider/Divider.js @@ -0,0 +1,36 @@ +import Box from '../Box' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' + +const Divider = { + name: 'Divider', + props: { + ...baseProps, + orientation: { + type: String, + default: 'horizontal' + } + }, + render (h) { + const borderProps = + this.orientation === 'vertical' + ? { borderLeft: '0.0625rem solid', height: 'auto', mx: 2 } + : { borderBottom: '0.0625rem solid', width: 'auto', my: 2 } + + return h(Box, { + props: { + ...forwardProps(this.$props), + ...borderProps, + as: 'hr', + border: 0, + opacity: 0.6, + borderColor: 'inherit' + }, + attrs: { + 'aria-orientation': this.orientation + } + }) + } +} + +export default Divider diff --git a/packages/chakra-ui-core/src/Divider/index.js b/packages/chakra-ui-core/src/Divider/index.js index 1e4f4130..1235cdd3 100644 --- a/packages/chakra-ui-core/src/Divider/index.js +++ b/packages/chakra-ui-core/src/Divider/index.js @@ -1,36 +1,2 @@ -import Box from '../Box' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' - -const Divider = { - name: 'Divider', - props: { - ...baseProps, - orientation: { - type: String, - default: 'horizontal' - } - }, - render (h) { - const borderProps = - this.orientation === 'vertical' - ? { borderLeft: '0.0625rem solid', height: 'auto', mx: 2 } - : { borderBottom: '0.0625rem solid', width: 'auto', my: 2 } - - return h(Box, { - props: { - ...forwardProps(this.$props), - ...borderProps, - as: 'hr', - border: 0, - opacity: 0.6, - borderColor: 'inherit' - }, - attrs: { - 'aria-orientation': this.orientation - } - }) - } -} - +import Divider from './Divider' export default Divider diff --git a/packages/chakra-ui-core/src/Drawer/Drawer.js b/packages/chakra-ui-core/src/Drawer/Drawer.js new file mode 100644 index 00000000..c555074b --- /dev/null +++ b/packages/chakra-ui-core/src/Drawer/Drawer.js @@ -0,0 +1,172 @@ +import styleProps, { baseProps } from '../config/props' +import { Modal, ModalContent, ModalBody, ModalHeader, ModalFooter, ModalOverlay, ModalCloseButton } from '../Modal' +import { forwardProps, HTMLElement } from '../utils' + +const Drawer = { + name: 'Drawer', + props: { + isOpen: { + type: Boolean, + default: false + }, + onClose: { + type: Function, + default: () => null + }, + closeOnEsc: { + type: Boolean, + default: true + }, + isFullHeight: { + type: Boolean, + default: true + }, + placement: { + type: String, + default: 'right' + }, + initialFocusRef: { + type: [HTMLElement, Object, String, Function], + default: () => null + }, + finalFocusRef: { + type: [HTMLElement, Object, String, Function], + default: () => null + }, + size: { + type: String, + default: 'xs' + }, + ...baseProps + }, + provide () { + return { + $DrawerContext: () => this.DrawerContext + } + }, + computed: { + DrawerContext () { + return { + size: this.size, + isOpen: this.isOpen, + placement: this.placement, + isFullHeight: this.isFullHeight + } + } + }, + render (h) { + return h(Modal, { + props: { + isOpen: this.isOpen, + onClose: this.onClose, + closeOnEsc: this.closeOnEsc, + initialFocusRef: this.initialFocusRef, + finalFocusRef: this.finalFocusRef, + formatIds: (id) => ({ + content: `drawer-${id}`, + header: `drawer-${id}-header`, + body: `drawer-${id}-body` + }), + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const getPlacementStyles = (position, { finalWidth, finalHeight }) => { + const placements = { + bottom: { + maxWidth: '100vw', + height: 'auto', + bottom: 0, + left: 0, + right: 0 + }, + top: { + maxWidth: '100vw', + height: 'auto', + top: 0, + left: 0, + right: 0 + }, + left: { + ...(finalWidth && { maxWidth: finalWidth }), + height: '100vh', + left: 0, + top: 0 + }, + right: { + ...(finalWidth && { maxWidth: finalWidth }), + right: 0, + top: 0, + height: '100vh' + } + } + + return placements[position] || placements['right'] +} + +const DrawerContent = { + name: 'DrawerContent', + props: { + ...baseProps + }, + inject: ['$DrawerContext'], + computed: { + context () { + return this.$DrawerContext() + } + }, + render (h) { + const { placement, isFullHeight } = this.context + const placementStyles = getPlacementStyles(placement, { + finalHeight: isFullHeight ? '100vh' : 'auto' + }) + + return h(ModalContent, { + props: { + noStyles: true, + position: 'fixed', + ...placementStyles, + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const DrawerOverlay = { + name: 'DrawerOverlay', + props: baseProps, + render (h) { + return h(ModalOverlay, { + props: forwardProps(this.$props) + }) + } +} + +const DrawerCloseButton = { + name: 'DrawerCloseButton', + props: styleProps, + render (h) { + return h(ModalCloseButton, { + props: { + position: 'fixed', + zIndex: '1', + ...forwardProps(this.$props) + }, + on: { + click: (e) => this.$emit('click', e) + } + }) + } +} + +export { + Drawer, + DrawerContent, + DrawerOverlay, + DrawerCloseButton, + ModalBody as DrawerBody, + ModalHeader as DrawerHeader, + ModalFooter as DrawerFooter +} diff --git a/packages/chakra-ui-core/src/Drawer/index.js b/packages/chakra-ui-core/src/Drawer/index.js index c555074b..04766cfa 100644 --- a/packages/chakra-ui-core/src/Drawer/index.js +++ b/packages/chakra-ui-core/src/Drawer/index.js @@ -1,172 +1 @@ -import styleProps, { baseProps } from '../config/props' -import { Modal, ModalContent, ModalBody, ModalHeader, ModalFooter, ModalOverlay, ModalCloseButton } from '../Modal' -import { forwardProps, HTMLElement } from '../utils' - -const Drawer = { - name: 'Drawer', - props: { - isOpen: { - type: Boolean, - default: false - }, - onClose: { - type: Function, - default: () => null - }, - closeOnEsc: { - type: Boolean, - default: true - }, - isFullHeight: { - type: Boolean, - default: true - }, - placement: { - type: String, - default: 'right' - }, - initialFocusRef: { - type: [HTMLElement, Object, String, Function], - default: () => null - }, - finalFocusRef: { - type: [HTMLElement, Object, String, Function], - default: () => null - }, - size: { - type: String, - default: 'xs' - }, - ...baseProps - }, - provide () { - return { - $DrawerContext: () => this.DrawerContext - } - }, - computed: { - DrawerContext () { - return { - size: this.size, - isOpen: this.isOpen, - placement: this.placement, - isFullHeight: this.isFullHeight - } - } - }, - render (h) { - return h(Modal, { - props: { - isOpen: this.isOpen, - onClose: this.onClose, - closeOnEsc: this.closeOnEsc, - initialFocusRef: this.initialFocusRef, - finalFocusRef: this.finalFocusRef, - formatIds: (id) => ({ - content: `drawer-${id}`, - header: `drawer-${id}-header`, - body: `drawer-${id}-body` - }), - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const getPlacementStyles = (position, { finalWidth, finalHeight }) => { - const placements = { - bottom: { - maxWidth: '100vw', - height: 'auto', - bottom: 0, - left: 0, - right: 0 - }, - top: { - maxWidth: '100vw', - height: 'auto', - top: 0, - left: 0, - right: 0 - }, - left: { - ...(finalWidth && { maxWidth: finalWidth }), - height: '100vh', - left: 0, - top: 0 - }, - right: { - ...(finalWidth && { maxWidth: finalWidth }), - right: 0, - top: 0, - height: '100vh' - } - } - - return placements[position] || placements['right'] -} - -const DrawerContent = { - name: 'DrawerContent', - props: { - ...baseProps - }, - inject: ['$DrawerContext'], - computed: { - context () { - return this.$DrawerContext() - } - }, - render (h) { - const { placement, isFullHeight } = this.context - const placementStyles = getPlacementStyles(placement, { - finalHeight: isFullHeight ? '100vh' : 'auto' - }) - - return h(ModalContent, { - props: { - noStyles: true, - position: 'fixed', - ...placementStyles, - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const DrawerOverlay = { - name: 'DrawerOverlay', - props: baseProps, - render (h) { - return h(ModalOverlay, { - props: forwardProps(this.$props) - }) - } -} - -const DrawerCloseButton = { - name: 'DrawerCloseButton', - props: styleProps, - render (h) { - return h(ModalCloseButton, { - props: { - position: 'fixed', - zIndex: '1', - ...forwardProps(this.$props) - }, - on: { - click: (e) => this.$emit('click', e) - } - }) - } -} - -export { - Drawer, - DrawerContent, - DrawerOverlay, - DrawerCloseButton, - ModalBody as DrawerBody, - ModalHeader as DrawerHeader, - ModalFooter as DrawerFooter -} +export * from './Drawer' diff --git a/packages/chakra-ui-core/src/Editable/Editable.js b/packages/chakra-ui-core/src/Editable/Editable.js new file mode 100644 index 00000000..e7de9d03 --- /dev/null +++ b/packages/chakra-ui-core/src/Editable/Editable.js @@ -0,0 +1,294 @@ +import styleProps, { baseProps } from '../config/props' +import { isDef, getElement, useId, forwardProps } from '../utils' +import Box from '../Box' +import PseudoBox from '../PseudoBox' + +const sharedEditableProps = { + fontSize: 'inherit', + fontWeight: 'inherit', + textAlign: 'inherit', + bg: 'transparent', + transition: 'all 0.2s', + borderRadius: 'md', + px: '3px', + mx: '-3px' +} + +const Editable = { + name: 'Editable', + props: { + ...baseProps, + value: String, + defaultValue: String, + isDisabled: Boolean, + startWithEditView: Boolean, + selectAllOnFocus: { + type: Boolean, + default: true + }, + submitOnBlur: { + type: Boolean, + default: true + }, + isPreviewFocusable: { + type: Boolean, + default: true + }, + placeholder: { + type: String, + default: 'Click to edit...' + } + }, + provide () { + return { + $EditableContext: () => this.EditableContext + } + }, + data () { + return { + isEditing: this.startWithEditView && !this.isDisabled, + innerValue: this.defaultValue || '', + previousValue: this._value, + inputNode: null + } + }, + computed: { + isControlled () { + return isDef(this.value) + }, + _value () { + return this.isControlled ? this.value : this.innerValue + }, + editableId () { + return `editable-${useId()}` + }, + EditableContext () { + return { + editableId: this.editableId, + isEditing: this.isEditing, + isDisabled: this.isDisabled, + placeholder: this.placeholder, + onRequestEdit: this.onRequestEdit, + submitOnBlur: this.submitOnBlur, + isPreviewFocusable: this.isPreviewFocusable, + value: this._value, + onKeyDown: this.handleKeyDown, + onChange: this.handleChange, + onSubmit: this.handleSubmit, + onCancel: this.handleCancel, + onFocus: this.handleFocus + } + } + }, + created () { + // Initialize previousValue to computed _value + this.previousValue = this._value + }, + mounted () { + this.$watch('isEditing', (newVal) => { + if (newVal) { + this.$emit('edit') + } + }) + + this.$watch(vm => [vm.isEditing, vm.selectAllOnFocus], () => { + this.$nextTick(() => { + this.inputNode = getElement(`#${this.editableId}`, this.$el) + if (this.isEditing && this.inputNode) { + this.inputNode.focus() + this.selectAllOnFocus && this.inputNode.select() + } + }) + }) + }, + methods: { + /** + * Handle cancel event + */ + handleCancel () { + this.isEditing = false + this.innerValue = this.previousValue + if (this.innerValue !== this.previousValue) { + this.$emit('change', this.previousValue) + } + this.$emit('cancel', this.previousValue) + }, + /** + * Handle submit event + */ + handleSubmit () { + this.isEditing = false + this.previousValue = this.innerValue + this.$emit('submit', this.innerValue) + }, + /** + * Handle change event + */ + handleChange (event) { + const { value } = event.target + if (!this.isControlled) { + this.innerValue = value + } + this.$emit('change', this.innerValue) + }, + /** + * Handle keydown event + */ + handleKeyDown (event) { + const { key } = event + if (key === 'Escape') { + this.handleCancel() + return + } + + if (key === 'Enter') { + this.handleSubmit() + } + }, + /** + * Handle focus event + */ + handleFocus (event) { + if (this.selectAllOnFocus) { + this.inputNode.select() + } + }, + /** + * Handle request editing + */ + onRequestEdit () { + if (!this.isDisabled) { + this.isEditing = true + } + } + }, + render (h) { + return h(Box, { + props: forwardProps(this.$props) + }, [ + this.$scopedSlots.default({ + isEditing: this.isEditing, + onSubmit: this.handleSubmit, + onCancel: this.handleCancel, + onRequestEdit: this.onRequestEdit + }) + ]) + } +} + +const EditablePreview = { + name: 'EditablePreview', + inject: ['$EditableContext'], + props: styleProps, + computed: { + context () { + return this.$EditableContext() + }, + hasValue () { + return isDef(this.context.value) && this.context.value !== '' + }, + styleProps () { + return { + ...sharedEditableProps, + cursor: 'text', + display: 'inline-block', + opacity: !this.hasValue ? 0.6 : undefined + } + }, + tabIndex () { + const { isEditing, isDisabled, isPreviewFocusable } = this.context + if ((!isEditing || !isDisabled) && isPreviewFocusable) { + return 0 + } + return null + } + }, + render (h) { + const { isEditing, isDisabled, onRequestEdit, value, placeholder } = this.context + if (isEditing) { + return null + } + + return h(PseudoBox, { + props: { + as: 'span', + ...this.styleProps, + ...forwardProps(this.$props) + }, + attrs: { + 'aria-disabled': isDisabled, + tabIndex: this.tabIndex + }, + nativeOn: { + focus: onRequestEdit + } + }, this.hasValue ? value : placeholder) + } +} + +const EditableInput = { + name: 'EditableInput', + inject: ['$EditableContext'], + props: styleProps, + computed: { + context () { + return this.$EditableContext() + }, + styleProps () { + return { + ...sharedEditableProps, + width: 'full', + _placeholder: { + opacity: '0.6' + } + } + } + }, + render (h) { + const { + isEditing, + editableId, + onChange, + onKeyDown, + value, + onSubmit, + submitOnBlur, + placeholder, + isDisabled + } = this.context + + if (!isEditing) { + return null + } + + return h(PseudoBox, { + props: { + as: 'input', + outline: 'none', + _focus: { + shadow: 'outline' + }, + ...this.styleProps, + ...forwardProps(this.$props) + }, + nativeOn: { + blur: event => { + submitOnBlur && onSubmit() + this.$emit('blur', event) + }, + input: onChange, + keydown: onKeyDown + }, + attrs: { + id: editableId, + disabled: isDisabled, + 'aria-disabled': isDisabled, + value, + placeholder + } + }, this.$slots.default) + } +} + +export default Editable +export { EditableInput, EditablePreview } diff --git a/packages/chakra-ui-core/src/Editable/index.js b/packages/chakra-ui-core/src/Editable/index.js index e7de9d03..b72f2f54 100644 --- a/packages/chakra-ui-core/src/Editable/index.js +++ b/packages/chakra-ui-core/src/Editable/index.js @@ -1,294 +1,3 @@ -import styleProps, { baseProps } from '../config/props' -import { isDef, getElement, useId, forwardProps } from '../utils' -import Box from '../Box' -import PseudoBox from '../PseudoBox' - -const sharedEditableProps = { - fontSize: 'inherit', - fontWeight: 'inherit', - textAlign: 'inherit', - bg: 'transparent', - transition: 'all 0.2s', - borderRadius: 'md', - px: '3px', - mx: '-3px' -} - -const Editable = { - name: 'Editable', - props: { - ...baseProps, - value: String, - defaultValue: String, - isDisabled: Boolean, - startWithEditView: Boolean, - selectAllOnFocus: { - type: Boolean, - default: true - }, - submitOnBlur: { - type: Boolean, - default: true - }, - isPreviewFocusable: { - type: Boolean, - default: true - }, - placeholder: { - type: String, - default: 'Click to edit...' - } - }, - provide () { - return { - $EditableContext: () => this.EditableContext - } - }, - data () { - return { - isEditing: this.startWithEditView && !this.isDisabled, - innerValue: this.defaultValue || '', - previousValue: this._value, - inputNode: null - } - }, - computed: { - isControlled () { - return isDef(this.value) - }, - _value () { - return this.isControlled ? this.value : this.innerValue - }, - editableId () { - return `editable-${useId()}` - }, - EditableContext () { - return { - editableId: this.editableId, - isEditing: this.isEditing, - isDisabled: this.isDisabled, - placeholder: this.placeholder, - onRequestEdit: this.onRequestEdit, - submitOnBlur: this.submitOnBlur, - isPreviewFocusable: this.isPreviewFocusable, - value: this._value, - onKeyDown: this.handleKeyDown, - onChange: this.handleChange, - onSubmit: this.handleSubmit, - onCancel: this.handleCancel, - onFocus: this.handleFocus - } - } - }, - created () { - // Initialize previousValue to computed _value - this.previousValue = this._value - }, - mounted () { - this.$watch('isEditing', (newVal) => { - if (newVal) { - this.$emit('edit') - } - }) - - this.$watch(vm => [vm.isEditing, vm.selectAllOnFocus], () => { - this.$nextTick(() => { - this.inputNode = getElement(`#${this.editableId}`, this.$el) - if (this.isEditing && this.inputNode) { - this.inputNode.focus() - this.selectAllOnFocus && this.inputNode.select() - } - }) - }) - }, - methods: { - /** - * Handle cancel event - */ - handleCancel () { - this.isEditing = false - this.innerValue = this.previousValue - if (this.innerValue !== this.previousValue) { - this.$emit('change', this.previousValue) - } - this.$emit('cancel', this.previousValue) - }, - /** - * Handle submit event - */ - handleSubmit () { - this.isEditing = false - this.previousValue = this.innerValue - this.$emit('submit', this.innerValue) - }, - /** - * Handle change event - */ - handleChange (event) { - const { value } = event.target - if (!this.isControlled) { - this.innerValue = value - } - this.$emit('change', this.innerValue) - }, - /** - * Handle keydown event - */ - handleKeyDown (event) { - const { key } = event - if (key === 'Escape') { - this.handleCancel() - return - } - - if (key === 'Enter') { - this.handleSubmit() - } - }, - /** - * Handle focus event - */ - handleFocus (event) { - if (this.selectAllOnFocus) { - this.inputNode.select() - } - }, - /** - * Handle request editing - */ - onRequestEdit () { - if (!this.isDisabled) { - this.isEditing = true - } - } - }, - render (h) { - return h(Box, { - props: forwardProps(this.$props) - }, [ - this.$scopedSlots.default({ - isEditing: this.isEditing, - onSubmit: this.handleSubmit, - onCancel: this.handleCancel, - onRequestEdit: this.onRequestEdit - }) - ]) - } -} - -const EditablePreview = { - name: 'EditablePreview', - inject: ['$EditableContext'], - props: styleProps, - computed: { - context () { - return this.$EditableContext() - }, - hasValue () { - return isDef(this.context.value) && this.context.value !== '' - }, - styleProps () { - return { - ...sharedEditableProps, - cursor: 'text', - display: 'inline-block', - opacity: !this.hasValue ? 0.6 : undefined - } - }, - tabIndex () { - const { isEditing, isDisabled, isPreviewFocusable } = this.context - if ((!isEditing || !isDisabled) && isPreviewFocusable) { - return 0 - } - return null - } - }, - render (h) { - const { isEditing, isDisabled, onRequestEdit, value, placeholder } = this.context - if (isEditing) { - return null - } - - return h(PseudoBox, { - props: { - as: 'span', - ...this.styleProps, - ...forwardProps(this.$props) - }, - attrs: { - 'aria-disabled': isDisabled, - tabIndex: this.tabIndex - }, - nativeOn: { - focus: onRequestEdit - } - }, this.hasValue ? value : placeholder) - } -} - -const EditableInput = { - name: 'EditableInput', - inject: ['$EditableContext'], - props: styleProps, - computed: { - context () { - return this.$EditableContext() - }, - styleProps () { - return { - ...sharedEditableProps, - width: 'full', - _placeholder: { - opacity: '0.6' - } - } - } - }, - render (h) { - const { - isEditing, - editableId, - onChange, - onKeyDown, - value, - onSubmit, - submitOnBlur, - placeholder, - isDisabled - } = this.context - - if (!isEditing) { - return null - } - - return h(PseudoBox, { - props: { - as: 'input', - outline: 'none', - _focus: { - shadow: 'outline' - }, - ...this.styleProps, - ...forwardProps(this.$props) - }, - nativeOn: { - blur: event => { - submitOnBlur && onSubmit() - this.$emit('blur', event) - }, - input: onChange, - keydown: onKeyDown - }, - attrs: { - id: editableId, - disabled: isDisabled, - 'aria-disabled': isDisabled, - value, - placeholder - } - }, this.$slots.default) - } -} - +import Editable from './Editable' export default Editable -export { EditableInput, EditablePreview } +export * from './Editable' diff --git a/packages/chakra-ui-core/src/Flex/Flex.js b/packages/chakra-ui-core/src/Flex/Flex.js new file mode 100644 index 00000000..81e24339 --- /dev/null +++ b/packages/chakra-ui-core/src/Flex/Flex.js @@ -0,0 +1,36 @@ +import Box from '../Box' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' + +/** + * Flex is Box with display: flex and comes with helpful style shorthand. It renders a div element. + */ +const Flex = { + name: 'Flex', + props: { + as: String, + align: [String, Array], + justify: [String, Array], + wrap: [String, Array], + direction: [String, Array], + size: [String, Array], + ...baseProps + }, + render (h) { + return h(Box, { + props: { + as: this.as, + display: 'flex', + flexDirection: this.direction, + alignItems: this.align, + justifyContent: this.justify, + flexWrap: this.wrap, + h: this.size, + w: this.size, + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +export default Flex diff --git a/packages/chakra-ui-core/src/Flex/index.js b/packages/chakra-ui-core/src/Flex/index.js index 81e24339..1a264789 100644 --- a/packages/chakra-ui-core/src/Flex/index.js +++ b/packages/chakra-ui-core/src/Flex/index.js @@ -1,36 +1,2 @@ -import Box from '../Box' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' - -/** - * Flex is Box with display: flex and comes with helpful style shorthand. It renders a div element. - */ -const Flex = { - name: 'Flex', - props: { - as: String, - align: [String, Array], - justify: [String, Array], - wrap: [String, Array], - direction: [String, Array], - size: [String, Array], - ...baseProps - }, - render (h) { - return h(Box, { - props: { - as: this.as, - display: 'flex', - flexDirection: this.direction, - alignItems: this.align, - justifyContent: this.justify, - flexWrap: this.wrap, - h: this.size, - w: this.size, - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - +import Flex from './Flex' export default Flex diff --git a/packages/chakra-ui-core/src/FormControl/FormControl.js b/packages/chakra-ui-core/src/FormControl/FormControl.js new file mode 100644 index 00000000..f1ecc26e --- /dev/null +++ b/packages/chakra-ui-core/src/FormControl/FormControl.js @@ -0,0 +1,77 @@ +import { baseProps } from '../config' +import Box from '../Box' +import { forwardProps } from '../utils' +import { formControlProps } from './formcontrol.props' + +const FormControl = { + name: 'FormControl', + props: { + ...baseProps, + ...formControlProps + }, + inject: { + $FormControlContext: { + default: null + } + }, + computed: { + formControlContext () { + if (!this.$FormControlContext) { + return this.props + } + return this.$FormControlContext() + }, + props () { + return { + isInvalid: this.isInvalid, + isRequired: this.isRequired, + isDisabled: this.isDisabled, + isReadOnly: this.isReadOnly, + id: this.$attrs.id + } + } + }, + provide () { + return { + $FormControlContext: () => this.props, + $useFormControl: this.useFormControl + } + }, + methods: { + useFormControl () { + /** + * If a in the ancestor tree, + * we provide it's values to this components' decendants. + * However, we give a higher precendence to prop values + * over context values. + */ + const context = this.formControlContext + if (!context) { + return this.props + } + + const keys = Object.keys(context) + return keys.reduce((acc, prop) => { + // We give precedence to `props` over `context` values + acc[prop] = this.props[prop] + + if (context) { + if (this.props[prop] == null) { + acc[prop] = context[prop] + } + } + return acc + }, {}) + } + }, + render (h) { + return h(Box, { + props: forwardProps(this.$props), + attrs: { + role: 'group' + } + }, this.$slots.default) + } +} + +export default FormControl diff --git a/packages/chakra-ui-core/src/FormControl/FormControl.stories.js b/packages/chakra-ui-core/src/FormControl/FormControl.stories.js index 8c9338de..23adc252 100644 --- a/packages/chakra-ui-core/src/FormControl/FormControl.stories.js +++ b/packages/chakra-ui-core/src/FormControl/FormControl.stories.js @@ -15,13 +15,13 @@ storiesOf('UI | FormControl', module) components: { Input, FormControl, Stack, Icon, InputGroup, InputLeftElement, InputRightElement, FormHelperText }, template: `
- + - Add your website here + Enter your password
`, @@ -35,19 +35,20 @@ storiesOf('UI | FormControl', module) components: { Input, FormControl, Stack, Icon, InputGroup, InputLeftElement, InputRightElement, FormErrorMessage }, template: `
- + - + - Website is invalid + Your password is too short.
`, data () { return { - shouldShowPassword: false + shouldShowPassword: false, + value: '123' } } })) diff --git a/packages/chakra-ui-core/src/FormControl/index.js b/packages/chakra-ui-core/src/FormControl/index.js index f1ecc26e..d900ea9e 100644 --- a/packages/chakra-ui-core/src/FormControl/index.js +++ b/packages/chakra-ui-core/src/FormControl/index.js @@ -1,77 +1,2 @@ -import { baseProps } from '../config' -import Box from '../Box' -import { forwardProps } from '../utils' -import { formControlProps } from './formcontrol.props' - -const FormControl = { - name: 'FormControl', - props: { - ...baseProps, - ...formControlProps - }, - inject: { - $FormControlContext: { - default: null - } - }, - computed: { - formControlContext () { - if (!this.$FormControlContext) { - return this.props - } - return this.$FormControlContext() - }, - props () { - return { - isInvalid: this.isInvalid, - isRequired: this.isRequired, - isDisabled: this.isDisabled, - isReadOnly: this.isReadOnly, - id: this.$attrs.id - } - } - }, - provide () { - return { - $FormControlContext: () => this.props, - $useFormControl: this.useFormControl - } - }, - methods: { - useFormControl () { - /** - * If a in the ancestor tree, - * we provide it's values to this components' decendants. - * However, we give a higher precendence to prop values - * over context values. - */ - const context = this.formControlContext - if (!context) { - return this.props - } - - const keys = Object.keys(context) - return keys.reduce((acc, prop) => { - // We give precedence to `props` over `context` values - acc[prop] = this.props[prop] - - if (context) { - if (this.props[prop] == null) { - acc[prop] = context[prop] - } - } - return acc - }, {}) - } - }, - render (h) { - return h(Box, { - props: forwardProps(this.$props), - attrs: { - role: 'group' - } - }, this.$slots.default) - } -} - +import FormControl from './FormControl' export default FormControl diff --git a/packages/chakra-ui-core/src/FormErrorMessage/FormErrorMessage.js b/packages/chakra-ui-core/src/FormErrorMessage/FormErrorMessage.js new file mode 100644 index 00000000..893dd860 --- /dev/null +++ b/packages/chakra-ui-core/src/FormErrorMessage/FormErrorMessage.js @@ -0,0 +1,67 @@ +import { baseProps } from '../config' +import { formControlProps } from '../FormControl/formcontrol.props' +import Flex from '../Flex' +import Icon from '../Icon' +import Text from '../Text' +import { forwardProps } from '../utils' + +const FormErrorMessage = { + name: 'FormErrorMessage', + inject: ['$colorMode', '$useFormControl'], + props: { + ...baseProps, + icon: { + type: String, + default: 'warning' + }, + ...formControlProps + }, + computed: { + formControl () { + return this.$useFormControl(this.$props) + }, + colorMode () { + return this.$colorMode() + } + }, + render (h) { + if (!this.formControl.isInvalid) { + return null + } + + const color = { + light: 'red.500', + dark: 'red.300' + } + + return h(Flex, { + props: { + color: color[this.colorMode], + mt: 2, + fontSize: 'sm', + align: 'center', + ...forwardProps(this.$props) + }, + attrs: { + id: this.formControl.id ? `${this.formControl.id}-error-message` : null + } + }, [ + h(Icon, { + props: { + name: this.icon, + mr: '0.5em' + }, + attrs: { + 'aria-hidden': true + } + }), + h(Text, { + props: { + lineHeight: 'normal' + } + }, this.$slots.default) + ]) + } +} + +export default FormErrorMessage diff --git a/packages/chakra-ui-core/src/FormErrorMessage/index.js b/packages/chakra-ui-core/src/FormErrorMessage/index.js index 82364a43..918b994c 100644 --- a/packages/chakra-ui-core/src/FormErrorMessage/index.js +++ b/packages/chakra-ui-core/src/FormErrorMessage/index.js @@ -1,67 +1,2 @@ -import { baseProps } from '../config' -import { formControlProps } from '../FormControl/formcontrol.props' -import Flex from '../Flex' -import Icon from '../Icon' -import Text from '../Text' -import { forwardProps } from '../utils' - -const FormErrorMessage = { - name: 'FormErrorMessage', - inject: ['$colorMode', '$useFormControl'], - props: { - ...baseProps, - icon: { - type: String, - default: 'warning' - }, - ...formControlProps - }, - computed: { - formControl () { - return this.$useFormControl(this.$props) - }, - colorMode () { - return this.$colorMode() - } - }, - render (h) { - if (!this.formControl.isInvalid) { - return null - } - - const color = { - light: 'red.500', - dark: 'red.300' - } - - return h(Flex, { - props: { - ...forwardProps(this.$props), - color: color[this.colorMode], - mt: 2, - fontSize: 'sm', - align: 'center' - }, - attrs: { - id: this.formControl.id ? `${this.formControl.id}-error-message` : null - } - }, [ - h(Icon, { - props: { - name: this.icon, - mr: '0.5em' - }, - attrs: { - 'aria-hidden': true - } - }), - h(Text, { - props: { - lineHeight: 'normal' - } - }, this.$slots.default) - ]) - } -} - +import FormErrorMessage from './FormErrorMessage' export default FormErrorMessage diff --git a/packages/chakra-ui-core/src/FormHelperText/FormHelperText.js b/packages/chakra-ui-core/src/FormHelperText/FormHelperText.js new file mode 100644 index 00000000..29d059f3 --- /dev/null +++ b/packages/chakra-ui-core/src/FormHelperText/FormHelperText.js @@ -0,0 +1,35 @@ +import Text from '../Text' +import { baseProps } from '../config' +import { forwardProps } from '../utils' + +const FormHelperText = { + name: 'FormHelperText', + inject: ['$useFormControl', '$colorMode'], + props: baseProps, + computed: { + formControl () { + return this.$useFormControl(this.$props) + }, + colorMode () { + return this.$colorMode() + } + }, + render (h) { + const color = { light: 'gray.500', dark: 'whiteAlpha.600' } + + return h(Text, { + props: { + ...forwardProps(this.$props), + mt: 2, + color: color[this.colorMode], + lineHeight: 'normal', + fontSize: 'sm' + }, + attrs: { + id: this.formControl.id ? `${this.formControl.id}-help-text` : null + } + }, this.$slots.default) + } +} + +export default FormHelperText diff --git a/packages/chakra-ui-core/src/FormHelperText/index.js b/packages/chakra-ui-core/src/FormHelperText/index.js index 29d059f3..858c6e55 100644 --- a/packages/chakra-ui-core/src/FormHelperText/index.js +++ b/packages/chakra-ui-core/src/FormHelperText/index.js @@ -1,35 +1,2 @@ -import Text from '../Text' -import { baseProps } from '../config' -import { forwardProps } from '../utils' - -const FormHelperText = { - name: 'FormHelperText', - inject: ['$useFormControl', '$colorMode'], - props: baseProps, - computed: { - formControl () { - return this.$useFormControl(this.$props) - }, - colorMode () { - return this.$colorMode() - } - }, - render (h) { - const color = { light: 'gray.500', dark: 'whiteAlpha.600' } - - return h(Text, { - props: { - ...forwardProps(this.$props), - mt: 2, - color: color[this.colorMode], - lineHeight: 'normal', - fontSize: 'sm' - }, - attrs: { - id: this.formControl.id ? `${this.formControl.id}-help-text` : null - } - }, this.$slots.default) - } -} - +import FormHelperText from './FormHelperText' export default FormHelperText diff --git a/packages/chakra-ui-core/src/FormLabel/FormLabel.js b/packages/chakra-ui-core/src/FormLabel/FormLabel.js new file mode 100644 index 00000000..ab3febbf --- /dev/null +++ b/packages/chakra-ui-core/src/FormLabel/FormLabel.js @@ -0,0 +1,73 @@ +import Box from '../Box' +import { baseProps } from '../config' +import { forwardProps } from '../utils' +import { formControlProps } from '../FormControl/formcontrol.props' + +const RequiredIndicator = { + name: 'RequiredIndicator', + inject: ['$colorMode'], + computed: { + colorMode () { + return this.$colorMode() + }, + color () { + const color = { light: 'red.500', dark: 'red.300' } + return color[this.colorMode] + } + }, + render (h) { + return h(Box, { + props: { + as: 'span', + ml: 1, + color: this.color + }, + attrs: { + 'aria-hidden': true + } + }, '*') + } +} + +const FormLabel = { + name: 'FormLabel', + inject: ['$useFormControl'], + props: { + ...baseProps, + ...formControlProps + }, + computed: { + formControlProps () { + return { + isInvalid: this.isInvalid, + isRequired: this.isRequired, + isDisabled: this.isDisabled, + isReadOnly: this.isReadOnly + } + }, + formControl () { + return this.$useFormControl(this.$props) + } + }, + render (h) { + return h(Box, { + props: { + as: 'label', + fontSize: 'md', + pr: '12px', + pb: '4px', + opacity: this.formControl.isDisabled ? '0.4' : '1', + fontWeight: 'medium', + textAlign: 'left', + verticalAlign: 'middle', + display: 'inline-block', + ...forwardProps(this.$props) + } + }, [ + ...this.$slots.default, + this.formControl.isRequired && h(RequiredIndicator) + ]) + } +} + +export default FormLabel diff --git a/packages/chakra-ui-core/src/FormLabel/index.js b/packages/chakra-ui-core/src/FormLabel/index.js index b68062f5..24476285 100644 --- a/packages/chakra-ui-core/src/FormLabel/index.js +++ b/packages/chakra-ui-core/src/FormLabel/index.js @@ -1,73 +1,2 @@ -import Box from '../Box' -import { baseProps } from '../config' -import { forwardProps } from '../utils' -import { formControlProps } from '../FormControl/formcontrol.props' - -const RequiredIndicator = { - name: 'RequiredIndicator', - inject: ['$colorMode'], - computed: { - colorMode () { - return this.$colorMode() - }, - color () { - const color = { light: 'red.500', dark: 'red.300' } - return color[this.colorMode] - } - }, - render (h) { - return h(Box, { - props: { - as: 'span', - ml: 1, - color: this.color - }, - attrs: { - 'aria-hidden': true - } - }, '*') - } -} - -const FormLabel = { - name: 'FormLabel', - inject: ['$useFormControl'], - props: { - ...baseProps, - ...formControlProps - }, - computed: { - formControlProps () { - return { - isInvalid: this.isInvalid, - isRequired: this.isRequired, - isDisabled: this.isDisabled, - isReadOnly: this.isReadOnly - } - }, - formControl () { - return this.$useFormControl(this.$props) - } - }, - render (h) { - return h(Box, { - props: { - ...forwardProps(this.$props), - as: 'label', - fontSize: 'md', - pr: '12px', - pb: '4px', - opacity: this.formControl.isDisabled ? '0.4' : '1', - fontWeight: 'medium', - textAlign: 'left', - verticalAlign: 'middle', - display: 'inline-block' - } - }, [ - ...this.$slots.default, - this.formControl.isRequired && h(RequiredIndicator) - ]) - } -} - +import FormLabel from './FormLabel' export default FormLabel diff --git a/packages/chakra-ui-core/src/Fragment/Fragment.js b/packages/chakra-ui-core/src/Fragment/Fragment.js new file mode 100644 index 00000000..2735f14a --- /dev/null +++ b/packages/chakra-ui-core/src/Fragment/Fragment.js @@ -0,0 +1,30 @@ +/** + * Fragment component to render multiple child sibling nodes in the place of + * their parent in the DOM. + * Note: This is a temporary solution to create fragments in Vue 2 + * until Vue 3 releases internal Fragment support + */ +const Fragment = { + name: 'Fragment', + directives: { + fragment: { + inserted (el) { + const fragment = document.createDocumentFragment() + Array.from(el.childNodes).forEach(child => + fragment.appendChild(child) + ) + el.parentNode.insertBefore(fragment, el) + el.parentNode.removeChild(el) + } + } + }, + render (h) { + // Here we render div but will remove it when node is inserted. + // And replace it with children + return h('div', { + directives: [{ name: 'fragment' }] + }, this.$slots.default) + } +} + +export default Fragment diff --git a/packages/chakra-ui-core/src/Fragment/index.js b/packages/chakra-ui-core/src/Fragment/index.js index 2735f14a..f3967bb9 100644 --- a/packages/chakra-ui-core/src/Fragment/index.js +++ b/packages/chakra-ui-core/src/Fragment/index.js @@ -1,30 +1,2 @@ -/** - * Fragment component to render multiple child sibling nodes in the place of - * their parent in the DOM. - * Note: This is a temporary solution to create fragments in Vue 2 - * until Vue 3 releases internal Fragment support - */ -const Fragment = { - name: 'Fragment', - directives: { - fragment: { - inserted (el) { - const fragment = document.createDocumentFragment() - Array.from(el.childNodes).forEach(child => - fragment.appendChild(child) - ) - el.parentNode.insertBefore(fragment, el) - el.parentNode.removeChild(el) - } - } - }, - render (h) { - // Here we render div but will remove it when node is inserted. - // And replace it with children - return h('div', { - directives: [{ name: 'fragment' }] - }, this.$slots.default) - } -} - +import Fragment from './Fragment' export default Fragment diff --git a/packages/chakra-ui-core/src/Grid/Grid.js b/packages/chakra-ui-core/src/Grid/Grid.js new file mode 100644 index 00000000..eab8565e --- /dev/null +++ b/packages/chakra-ui-core/src/Grid/Grid.js @@ -0,0 +1,45 @@ +import Box from '../Box' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' +import { SNA } from '../config/props/props.types' + +const Grid = { + name: 'Grid', + props: { + gap: SNA, + rowGap: SNA, + columnGap: SNA, + autoFlow: SNA, + autoRows: SNA, + autoColumns: SNA, + templateRows: SNA, + templateColumns: SNA, + templateAreas: SNA, + area: SNA, + column: SNA, + row: SNA, + ...baseProps + }, + render (h) { + return h(Box, { + props: { + d: 'grid', + gridArea: this.area, + gridTemplateAreas: this.templateAreas, + gridGap: this.gap, + gridRowGap: this.rowGap, + gridColumnGap: this.columnGap, + gridAutoColumns: this.autoColumns, + gridColumn: this.column, + gridRow: this.row, + gridAutoFlow: this.autoFlow, + gridAutoRows: this.autoRows, + gridTemplateRows: this.templateRows, + gridTemplateColumns: this.templateColumns, + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +export default Grid diff --git a/packages/chakra-ui-core/src/Grid/index.js b/packages/chakra-ui-core/src/Grid/index.js index 575a23e1..7b0677d3 100644 --- a/packages/chakra-ui-core/src/Grid/index.js +++ b/packages/chakra-ui-core/src/Grid/index.js @@ -1,68 +1,2 @@ -import Box from '../Box' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' - -const Grid = { - name: 'Grid', - props: { - gap: { - type: [String, Number, Array] - }, - rowGap: { - type: [String, Number, Array] - }, - columnGap: { - type: [String, Number, Array] - }, - autoFlow: { - type: [String, Number, Array] - }, - autoRows: { - type: [String, Number, Array] - }, - autoColumns: { - type: [String, Number, Array] - }, - templateRows: { - type: [String, Number, Array] - }, - templateColumns: { - type: [String, Number, Array] - }, - templateAreas: { - type: [String, Number, Array] - }, - area: { - type: [String, Number, Array] - }, - column: { - type: [String, Number, Array] - }, - row: { - type: [String, Number, Array] - }, - ...baseProps - }, - render (h) { - return h(Box, { - props: { - d: 'grid', - gridArea: this.area, - gridTemplateAreas: this.templateAreas, - gridGap: this.gap, - gridRowGap: this.rowGap, - gridColumnGap: this.columnGap, - gridAutoColumns: this.autoColumns, - gridColumn: this.column, - gridRow: this.row, - gridAutoFlow: this.autoFlow, - gridAutoRows: this.autoRows, - gridTemplateRows: this.templateRows, - gridTemplateColumns: this.templateColumns, - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - +import Grid from './Grid' export default Grid diff --git a/packages/chakra-ui-core/src/Heading/Heading.js b/packages/chakra-ui-core/src/Heading/Heading.js new file mode 100644 index 00000000..c2981f2b --- /dev/null +++ b/packages/chakra-ui-core/src/Heading/Heading.js @@ -0,0 +1,44 @@ +import Box from '../Box' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' + +const sizes = { + '2xl': ['4xl', null, '5xl'], + xl: ['3xl', null, '4xl'], + lg: ['xl', null, '2xl'], + md: 'xl', + sm: 'md', + xs: 'sm' +} + +/** + * Heading component gives text elements for titles and subtitles + */ +const Heading = { + name: 'Heading', + props: { + size: { + type: [String, Array, Object], + default: 'xl' + }, + as: { + type: String, + default: 'h1' + }, + ...baseProps + }, + render (h) { + return h(Box, { + props: { + as: this.as, + fontSize: sizes[this.size], + lineHeight: 'shorter', + fontWeight: 'bold', + fontFamily: 'heading', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +export default Heading diff --git a/packages/chakra-ui-core/src/Heading/index.js b/packages/chakra-ui-core/src/Heading/index.js index c2981f2b..7b726512 100644 --- a/packages/chakra-ui-core/src/Heading/index.js +++ b/packages/chakra-ui-core/src/Heading/index.js @@ -1,44 +1,2 @@ -import Box from '../Box' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' - -const sizes = { - '2xl': ['4xl', null, '5xl'], - xl: ['3xl', null, '4xl'], - lg: ['xl', null, '2xl'], - md: 'xl', - sm: 'md', - xs: 'sm' -} - -/** - * Heading component gives text elements for titles and subtitles - */ -const Heading = { - name: 'Heading', - props: { - size: { - type: [String, Array, Object], - default: 'xl' - }, - as: { - type: String, - default: 'h1' - }, - ...baseProps - }, - render (h) { - return h(Box, { - props: { - as: this.as, - fontSize: sizes[this.size], - lineHeight: 'shorter', - fontWeight: 'bold', - fontFamily: 'heading', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - +import Heading from './Heading' export default Heading diff --git a/packages/chakra-ui-core/src/Icon/Icon.js b/packages/chakra-ui-core/src/Icon/Icon.js new file mode 100644 index 00000000..cdd96432 --- /dev/null +++ b/packages/chakra-ui-core/src/Icon/Icon.js @@ -0,0 +1,78 @@ +import { css } from 'emotion' +import Box from '../Box' +import iconPaths from '../lib/internal-icons' +import { forwardProps } from '../utils' +import { baseProps } from '../config/props' +import { iconProps } from './icon.props' + +const fallbackIcon = iconPaths['question-outline'] + +const Svg = { + name: 'IconSvg', + props: { + ...iconProps, + ...baseProps + }, + render (h) { + const className = css` + flex-shrink: 0; + backface-visibility: hidden; + &:not(:root) { + overflow: hidden; + } + ` + return h(Box, { + props: { + as: 'svg', + ...forwardProps(this.$props) + }, + class: [className] + }, this.$slots.default) + } +} + +/** + * The Icon component renders SVGs for visual aid + */ +export default { + name: 'Icon', + inject: ['$theme', '$icons'], + props: { + ...iconProps, + ...baseProps + }, + render (h) { + let icon, viewBox + if (this.name) { + icon = this.$icons[this.name] + } else { + console.warn(`[Chakra]: You need to provide the "name" or "use" prop to for the Icon component`) + } + + if (!icon) { + icon = fallbackIcon + } + + viewBox = icon.viewBox || '0 0 24 24' + + return h(Svg, { + props: { + as: 'svg', + w: this.size, + h: this.size, + color: this.color, + d: 'inline-block', + verticalAlign: 'middle', + ...forwardProps(this.$props) + }, + attrs: { + viewBox, + role: 'presentation', + focusable: false + }, + domProps: { + innerHTML: icon.path + } + }) + } +} diff --git a/packages/chakra-ui-core/src/Icon/index.js b/packages/chakra-ui-core/src/Icon/index.js index cdd96432..579813e9 100644 --- a/packages/chakra-ui-core/src/Icon/index.js +++ b/packages/chakra-ui-core/src/Icon/index.js @@ -1,78 +1,2 @@ -import { css } from 'emotion' -import Box from '../Box' -import iconPaths from '../lib/internal-icons' -import { forwardProps } from '../utils' -import { baseProps } from '../config/props' -import { iconProps } from './icon.props' - -const fallbackIcon = iconPaths['question-outline'] - -const Svg = { - name: 'IconSvg', - props: { - ...iconProps, - ...baseProps - }, - render (h) { - const className = css` - flex-shrink: 0; - backface-visibility: hidden; - &:not(:root) { - overflow: hidden; - } - ` - return h(Box, { - props: { - as: 'svg', - ...forwardProps(this.$props) - }, - class: [className] - }, this.$slots.default) - } -} - -/** - * The Icon component renders SVGs for visual aid - */ -export default { - name: 'Icon', - inject: ['$theme', '$icons'], - props: { - ...iconProps, - ...baseProps - }, - render (h) { - let icon, viewBox - if (this.name) { - icon = this.$icons[this.name] - } else { - console.warn(`[Chakra]: You need to provide the "name" or "use" prop to for the Icon component`) - } - - if (!icon) { - icon = fallbackIcon - } - - viewBox = icon.viewBox || '0 0 24 24' - - return h(Svg, { - props: { - as: 'svg', - w: this.size, - h: this.size, - color: this.color, - d: 'inline-block', - verticalAlign: 'middle', - ...forwardProps(this.$props) - }, - attrs: { - viewBox, - role: 'presentation', - focusable: false - }, - domProps: { - innerHTML: icon.path - } - }) - } -} +import Icon from './Icon' +export default Icon diff --git a/packages/chakra-ui-core/src/IconButton/IconButton.js b/packages/chakra-ui-core/src/IconButton/IconButton.js new file mode 100644 index 00000000..d323b058 --- /dev/null +++ b/packages/chakra-ui-core/src/IconButton/IconButton.js @@ -0,0 +1,80 @@ +import Button from '../Button' +import Icon from '../Icon' +import Box from '../Box' +import styleProps from '../config/props' +import { forwardProps } from '../utils' +import { buttonProps } from '../Button/button.props' + +const baseStyles = { + display: 'inline-flex', + appearance: 'none', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 250ms', + userSelect: 'none', + position: 'relative', + whiteSpace: 'nowrap', + verticalAlign: 'middle', + lineHeight: '1.2', + outline: 'none' +} + +export default { + name: 'IconButton', + inject: ['$theme', '$colorMode'], + props: { + icon: { + type: [String] + }, + isRound: { + type: [Boolean] + }, + _ariaLabel: { + type: [String] + }, + ...buttonProps, + ...styleProps + }, + render (h) { + const { isFullWidth, leftIcon, rightIcon, loadingText, ...props } = this.$props + + return h(Button, { + props: { + p: 0, + rounded: this.isRound ? 'full' : 'md', + size: this.size, + ...forwardProps(props) + }, + attrs: { + 'aria-label': this._ariaLabel + }, + on: { + click: (e) => this.$emit('click', e) + } + }, + [typeof this.icon === 'string' + ? h(Icon, { + props: { + ...baseStyles, + name: this.icon, + color: 'currentColor', + mb: '2px', + size: '1em' + }, + attrs: { + focusable: false, + 'aria-hidden': true + } + }) + : h(Box, { + props: { + as: this.icon, + color: 'currentColor' + }, + attrs: { + focusable: true + } + })] + ) + } +} diff --git a/packages/chakra-ui-core/src/IconButton/index.js b/packages/chakra-ui-core/src/IconButton/index.js index d323b058..6677ecde 100644 --- a/packages/chakra-ui-core/src/IconButton/index.js +++ b/packages/chakra-ui-core/src/IconButton/index.js @@ -1,80 +1,2 @@ -import Button from '../Button' -import Icon from '../Icon' -import Box from '../Box' -import styleProps from '../config/props' -import { forwardProps } from '../utils' -import { buttonProps } from '../Button/button.props' - -const baseStyles = { - display: 'inline-flex', - appearance: 'none', - alignItems: 'center', - justifyContent: 'center', - transition: 'all 250ms', - userSelect: 'none', - position: 'relative', - whiteSpace: 'nowrap', - verticalAlign: 'middle', - lineHeight: '1.2', - outline: 'none' -} - -export default { - name: 'IconButton', - inject: ['$theme', '$colorMode'], - props: { - icon: { - type: [String] - }, - isRound: { - type: [Boolean] - }, - _ariaLabel: { - type: [String] - }, - ...buttonProps, - ...styleProps - }, - render (h) { - const { isFullWidth, leftIcon, rightIcon, loadingText, ...props } = this.$props - - return h(Button, { - props: { - p: 0, - rounded: this.isRound ? 'full' : 'md', - size: this.size, - ...forwardProps(props) - }, - attrs: { - 'aria-label': this._ariaLabel - }, - on: { - click: (e) => this.$emit('click', e) - } - }, - [typeof this.icon === 'string' - ? h(Icon, { - props: { - ...baseStyles, - name: this.icon, - color: 'currentColor', - mb: '2px', - size: '1em' - }, - attrs: { - focusable: false, - 'aria-hidden': true - } - }) - : h(Box, { - props: { - as: this.icon, - color: 'currentColor' - }, - attrs: { - focusable: true - } - })] - ) - } -} +import IconButton from './IconButton' +export default IconButton diff --git a/packages/chakra-ui-core/src/Image/Image.js b/packages/chakra-ui-core/src/Image/Image.js new file mode 100644 index 00000000..83328656 --- /dev/null +++ b/packages/chakra-ui-core/src/Image/Image.js @@ -0,0 +1,68 @@ +import { baseProps } from '../config/props' +import Box from '../Box' +import NoSsr from '../NoSsr' +import { forwardProps } from '../utils' + +const Image = { + name: 'CImage', + props: { + ...baseProps, + src: String, + fallbackSrc: String, + ignoreFalback: Boolean, + htmlWidth: String, + htmlHeight: String + }, + data () { + return { + image: undefined, + hasLoaded: false + } + }, + created () { + // Should only invoke window.Image in the browser. + if (process.browser) { + this.loadImage(this.src) + } + }, + methods: { + loadImage (src) { + const image = new window.Image() + image.src = src + + image.onload = event => { + this.hasLoaded = true + this.$emit('load', event) + } + + image.onError = event => { + this.hasLoaded = false + this.$emit('error', event) + } + } + }, + render (h) { + let imageProps + if (this.ignoreFallback) { + imageProps = { src: this.src } + } else { + imageProps = { src: this.hasLoaded ? this.src : this.fallbackSrc } + } + return h(NoSsr, [ + h(Box, { + props: { + ...forwardProps(this.$props), + as: 'img' + }, + attrs: { + ...imageProps, + ...this.$attrs, + width: this.htmlWidth, + height: this.htmlHeight + } + }) + ]) + } +} + +export default Image diff --git a/packages/chakra-ui-core/src/Image/index.js b/packages/chakra-ui-core/src/Image/index.js index 83328656..08cda898 100644 --- a/packages/chakra-ui-core/src/Image/index.js +++ b/packages/chakra-ui-core/src/Image/index.js @@ -1,68 +1,2 @@ -import { baseProps } from '../config/props' -import Box from '../Box' -import NoSsr from '../NoSsr' -import { forwardProps } from '../utils' - -const Image = { - name: 'CImage', - props: { - ...baseProps, - src: String, - fallbackSrc: String, - ignoreFalback: Boolean, - htmlWidth: String, - htmlHeight: String - }, - data () { - return { - image: undefined, - hasLoaded: false - } - }, - created () { - // Should only invoke window.Image in the browser. - if (process.browser) { - this.loadImage(this.src) - } - }, - methods: { - loadImage (src) { - const image = new window.Image() - image.src = src - - image.onload = event => { - this.hasLoaded = true - this.$emit('load', event) - } - - image.onError = event => { - this.hasLoaded = false - this.$emit('error', event) - } - } - }, - render (h) { - let imageProps - if (this.ignoreFallback) { - imageProps = { src: this.src } - } else { - imageProps = { src: this.hasLoaded ? this.src : this.fallbackSrc } - } - return h(NoSsr, [ - h(Box, { - props: { - ...forwardProps(this.$props), - as: 'img' - }, - attrs: { - ...imageProps, - ...this.$attrs, - width: this.htmlWidth, - height: this.htmlHeight - } - }) - ]) - } -} - +import Image from './Image' export default Image diff --git a/packages/chakra-ui-core/src/Input/Input.js b/packages/chakra-ui-core/src/Input/Input.js new file mode 100644 index 00000000..dba76a1c --- /dev/null +++ b/packages/chakra-ui-core/src/Input/Input.js @@ -0,0 +1,91 @@ +import styleProps from '../config/props' +import PseudoBox from '../PseudoBox' +import useInputStyle from './input.styles' +import { forwardProps } from '../utils' +import { inputProps } from './input.props' + +const Input = { + // We prefix the input name because we need to compare the + // VNode names inside InputGroup component + name: 'CInput', + inject: { + '$colorMode': { + default: 'light' + }, + '$theme': { + default: () => ({}) + }, + '$useFormControl': { + default: null + } + }, + model: { + prop: 'value', + event: 'input' + }, + props: { + ...styleProps, + ...inputProps + }, + computed: { + colorMode () { + return this.$colorMode() + }, + theme () { + return this.$theme() + }, + formControl () { + if (!this.$useFormControl) { + return { + isReadOnly: this.isReadOnly, + isDisabled: this.isDisabled, + isInvalid: this.isInvalid, + isRequired: this.isRequired + } + } + return this.$useFormControl(this.$props) + } + }, + methods: { + emitValue (event) { + this.$emit('input', event.target.value, event) + this.$emit('change', event) + } + }, + render (h) { + const inputStyles = useInputStyle({ + ...this.$props, + theme: this.theme, + colorMode: this.colorMode + }) + + return h(PseudoBox, { + props: { + ...inputStyles, + as: this.as, + fontFamily: 'body', + ...forwardProps(this.$props) + }, + domProps: { + value: this.value + }, + attrs: { + 'aria-readonly': this.isReadOnly, + 'read-only': this.formControl.isReadOnly, + disabled: this.formControl.isDisabled, + 'aria-disabled': this.formControl.isDisabled, + 'aria-label': this._ariaLabel, + 'aria-describedby': this._ariaDescribedby, + 'aria-invalid': this.formControl.isInvalid, + required: this.formControl.isRequired, + 'aria-required': this.formControl.isRequired + }, + nativeOn: { + input: this.emitValue + }, + ref: 'input' + }, this.$slots.default) + } +} + +export default Input diff --git a/packages/chakra-ui-core/src/Input/Input.stories.js b/packages/chakra-ui-core/src/Input/Input.stories.js index 54f7612d..74f75d5b 100644 --- a/packages/chakra-ui-core/src/Input/Input.stories.js +++ b/packages/chakra-ui-core/src/Input/Input.stories.js @@ -20,24 +20,20 @@ storiesOf('UI | Input', module) /> ` })) - -const variantStories = storiesOf('Input/Variants', module) - -variantStories.add('Filled', () => ({ - components: { Input }, - template: ` - -` -})) - -variantStories.add('Filled', () => ({ - components: { Input }, - template: ` - + .add('Filled', () => ({ + components: { Input }, + template: ` + ` -})) + })) + .add('Filled w/ custom focus and error border colors', () => ({ + components: { Input }, + template: ` + + ` + })) diff --git a/packages/chakra-ui-core/src/Input/index.js b/packages/chakra-ui-core/src/Input/index.js index dba76a1c..e01a0da0 100644 --- a/packages/chakra-ui-core/src/Input/index.js +++ b/packages/chakra-ui-core/src/Input/index.js @@ -1,91 +1,2 @@ -import styleProps from '../config/props' -import PseudoBox from '../PseudoBox' -import useInputStyle from './input.styles' -import { forwardProps } from '../utils' -import { inputProps } from './input.props' - -const Input = { - // We prefix the input name because we need to compare the - // VNode names inside InputGroup component - name: 'CInput', - inject: { - '$colorMode': { - default: 'light' - }, - '$theme': { - default: () => ({}) - }, - '$useFormControl': { - default: null - } - }, - model: { - prop: 'value', - event: 'input' - }, - props: { - ...styleProps, - ...inputProps - }, - computed: { - colorMode () { - return this.$colorMode() - }, - theme () { - return this.$theme() - }, - formControl () { - if (!this.$useFormControl) { - return { - isReadOnly: this.isReadOnly, - isDisabled: this.isDisabled, - isInvalid: this.isInvalid, - isRequired: this.isRequired - } - } - return this.$useFormControl(this.$props) - } - }, - methods: { - emitValue (event) { - this.$emit('input', event.target.value, event) - this.$emit('change', event) - } - }, - render (h) { - const inputStyles = useInputStyle({ - ...this.$props, - theme: this.theme, - colorMode: this.colorMode - }) - - return h(PseudoBox, { - props: { - ...inputStyles, - as: this.as, - fontFamily: 'body', - ...forwardProps(this.$props) - }, - domProps: { - value: this.value - }, - attrs: { - 'aria-readonly': this.isReadOnly, - 'read-only': this.formControl.isReadOnly, - disabled: this.formControl.isDisabled, - 'aria-disabled': this.formControl.isDisabled, - 'aria-label': this._ariaLabel, - 'aria-describedby': this._ariaDescribedby, - 'aria-invalid': this.formControl.isInvalid, - required: this.formControl.isRequired, - 'aria-required': this.formControl.isRequired - }, - nativeOn: { - input: this.emitValue - }, - ref: 'input' - }, this.$slots.default) - } -} - +import Input from './Input' export default Input diff --git a/packages/chakra-ui-core/src/InputAddon/InputAddon.js b/packages/chakra-ui-core/src/InputAddon/InputAddon.js new file mode 100644 index 00000000..bf045fa2 --- /dev/null +++ b/packages/chakra-ui-core/src/InputAddon/InputAddon.js @@ -0,0 +1,94 @@ +import Box from '../Box' +import styleProps from '../config/props' +import useInputStyle from '../Input/input.styles' +import { forwardProps } from '../utils' + +const addonProps = { + ...styleProps, + placement: { + type: String, + default: 'left' + }, + size: { + type: String, + default: 'md' + } +} + +const InputAddon = { + name: 'InputAddon', + inject: ['$colorMode', '$theme'], + props: addonProps, + computed: { + colorMode () { + return this.$colorMode() + }, + theme () { + return this.$theme() + } + }, + render (h) { + const bg = { dark: 'whiteAlpha.300', light: 'gray.100' } + const _placement = { + left: { + mr: '-1px', + roundedRight: 0, + borderRightColor: 'transparent' + }, + right: { + order: 1, + roundedLeft: 0, + borderLeftColor: 'transparent' + } + } + + const styleProps = { + ...useInputStyle({ + size: this.size, + variant: 'outline', + colorMode: this.colorMode, + theme: this.theme + }), + flex: '0 0 auto', + whiteSpace: 'nowrap', + bg: bg[this.colorMode], + ..._placement[this.placement] + } + + return h(Box, { + props: { + ...forwardProps(this.$props), + ...styleProps + } + }, this.$slots.default) + } +} + +const InputLeftAddon = { + name: 'InputLeftAddon', + props: addonProps, + render (h) { + return h(InputAddon, { + props: { + ...forwardProps(this.$props), + placement: 'left' + } + }, this.$slots.default) + } +} + +const InputRightAddon = { + name: 'InputRightAddon', + props: addonProps, + render (h) { + return h(InputAddon, { + props: { + ...forwardProps(this.$props), + placement: 'right' + } + }, this.$slots.default) + } +} + +export default InputAddon +export { InputLeftAddon, InputRightAddon } diff --git a/packages/chakra-ui-core/src/InputAddon/index.js b/packages/chakra-ui-core/src/InputAddon/index.js index bf045fa2..beeaa762 100644 --- a/packages/chakra-ui-core/src/InputAddon/index.js +++ b/packages/chakra-ui-core/src/InputAddon/index.js @@ -1,94 +1,3 @@ -import Box from '../Box' -import styleProps from '../config/props' -import useInputStyle from '../Input/input.styles' -import { forwardProps } from '../utils' - -const addonProps = { - ...styleProps, - placement: { - type: String, - default: 'left' - }, - size: { - type: String, - default: 'md' - } -} - -const InputAddon = { - name: 'InputAddon', - inject: ['$colorMode', '$theme'], - props: addonProps, - computed: { - colorMode () { - return this.$colorMode() - }, - theme () { - return this.$theme() - } - }, - render (h) { - const bg = { dark: 'whiteAlpha.300', light: 'gray.100' } - const _placement = { - left: { - mr: '-1px', - roundedRight: 0, - borderRightColor: 'transparent' - }, - right: { - order: 1, - roundedLeft: 0, - borderLeftColor: 'transparent' - } - } - - const styleProps = { - ...useInputStyle({ - size: this.size, - variant: 'outline', - colorMode: this.colorMode, - theme: this.theme - }), - flex: '0 0 auto', - whiteSpace: 'nowrap', - bg: bg[this.colorMode], - ..._placement[this.placement] - } - - return h(Box, { - props: { - ...forwardProps(this.$props), - ...styleProps - } - }, this.$slots.default) - } -} - -const InputLeftAddon = { - name: 'InputLeftAddon', - props: addonProps, - render (h) { - return h(InputAddon, { - props: { - ...forwardProps(this.$props), - placement: 'left' - } - }, this.$slots.default) - } -} - -const InputRightAddon = { - name: 'InputRightAddon', - props: addonProps, - render (h) { - return h(InputAddon, { - props: { - ...forwardProps(this.$props), - placement: 'right' - } - }, this.$slots.default) - } -} - +import InputAddon from './InputAddon' export default InputAddon -export { InputLeftAddon, InputRightAddon } +export * from './InputAddon' diff --git a/packages/chakra-ui-core/src/InputElement/InputElement.js b/packages/chakra-ui-core/src/InputElement/InputElement.js new file mode 100644 index 00000000..ef4c8058 --- /dev/null +++ b/packages/chakra-ui-core/src/InputElement/InputElement.js @@ -0,0 +1,70 @@ +import { baseProps } from '../config' +import { inputSizes } from '../Input/input.styles' +import Box from '../Box' +import { forwardProps } from '../utils' + +const props = { + ...baseProps, + size: String, + placement: { + type: String, + default: 'left' + }, + disablePointerEvents: Boolean +} + +const InputElement = { + name: 'InputElement', + props, + render (h) { + const height = inputSizes[this.size] && inputSizes[this.size]['height'] + const fontSize = inputSizes[this.size] && inputSizes[this.size]['fontSize'] + const placementProp = { [this.placement]: '0' } + + return h(Box, { + props: { + ...forwardProps(this.$props), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + position: 'absolute', + width: height, + height, + fontSize, + top: 0, + zIndex: 2, + ...(this.disablePointerEvents && { pointerEvents: 'none' }), + ...placementProp + } + }, this.$slots.default) + } +} + +const InputLeftElement = { + name: 'InputLeftElement', + props, + render (h) { + return h(InputElement, { + props: { + ...forwardProps(this.$props), + placement: 'left' + } + }, this.$slots.default) + } +} + +const InputRightElement = { + name: 'InputRightElement', + props, + render (h) { + return h(InputElement, { + props: { + ...forwardProps(this.$props), + placement: 'right' + } + }, this.$slots.default) + } +} + +export default InputElement +export { InputLeftElement, InputRightElement } diff --git a/packages/chakra-ui-core/src/InputElement/index.js b/packages/chakra-ui-core/src/InputElement/index.js index ef4c8058..a4118e65 100644 --- a/packages/chakra-ui-core/src/InputElement/index.js +++ b/packages/chakra-ui-core/src/InputElement/index.js @@ -1,70 +1,3 @@ -import { baseProps } from '../config' -import { inputSizes } from '../Input/input.styles' -import Box from '../Box' -import { forwardProps } from '../utils' - -const props = { - ...baseProps, - size: String, - placement: { - type: String, - default: 'left' - }, - disablePointerEvents: Boolean -} - -const InputElement = { - name: 'InputElement', - props, - render (h) { - const height = inputSizes[this.size] && inputSizes[this.size]['height'] - const fontSize = inputSizes[this.size] && inputSizes[this.size]['fontSize'] - const placementProp = { [this.placement]: '0' } - - return h(Box, { - props: { - ...forwardProps(this.$props), - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - position: 'absolute', - width: height, - height, - fontSize, - top: 0, - zIndex: 2, - ...(this.disablePointerEvents && { pointerEvents: 'none' }), - ...placementProp - } - }, this.$slots.default) - } -} - -const InputLeftElement = { - name: 'InputLeftElement', - props, - render (h) { - return h(InputElement, { - props: { - ...forwardProps(this.$props), - placement: 'left' - } - }, this.$slots.default) - } -} - -const InputRightElement = { - name: 'InputRightElement', - props, - render (h) { - return h(InputElement, { - props: { - ...forwardProps(this.$props), - placement: 'right' - } - }, this.$slots.default) - } -} - +import InputElement from './InputElement' export default InputElement -export { InputLeftElement, InputRightElement } +export * from './InputElement' diff --git a/packages/chakra-ui-core/src/InputGroup/InputGroup.js b/packages/chakra-ui-core/src/InputGroup/InputGroup.js new file mode 100644 index 00000000..149953fb --- /dev/null +++ b/packages/chakra-ui-core/src/InputGroup/InputGroup.js @@ -0,0 +1,75 @@ +import { StringArray } from '../config/props/props.types' +import { baseProps } from '../config' +import { inputSizes } from '../Input/input.styles' +import Box from '../Box' +import { cloneVNode, forwardProps } from '../utils' +import { InputLeftElement, InputRightElement } from '../InputElement' +import Input from '../Input' + +const InputGroup = { + name: 'InputGroup', + inject: ['$theme'], + props: { + ...baseProps, + size: { + type: StringArray, + default: 'md' + } + }, + computed: { + theme () { + return this.$theme() + } + }, + render (h) { + const { sizes } = this.theme + let pl = null + let pr = null + const height = inputSizes[this.size] && inputSizes[this.size]['height'] + const children = this.$slots.default.filter(e => e.tag) + const clones = children + .map((vnode) => { + if (vnode.tag.includes(InputLeftElement.name)) { + pl = sizes[height] + } + if (vnode.tag.includes(InputRightElement.name)) { + pr = sizes[height] + } + if (vnode.tag.includes(Input.name)) { + const clone = cloneVNode(vnode, h) + return h(clone.componentOptions.Ctor, { + ...clone.data, + ...(clone.componentOptions.listeners || {}), + props: { + ...(clone.data.props || {}), + ...clone.componentOptions.propsData, + borderRadius: clone.componentOptions.propsData.rounded, + size: this.size, + paddingLeft: clone.componentOptions.propsData.pl || pl, + paddingRight: clone.componentOptions.propsData.pr || pr + } + }, vnode.componentOptions.children) + } + const clone = cloneVNode(vnode, h) + return h(clone.componentOptions.Ctor, { + ...clone.data, + ...(clone.componentOptions.listeners || {}), + props: { + ...(clone.data.props || {}), + ...clone.componentOptions.propsData, + size: this.size + } + }, vnode.componentOptions.children) + }) + + return h(Box, { + props: { + ...forwardProps(this.$props), + display: 'flex', + position: 'relative' + } + }, clones) + } +} + +export default InputGroup diff --git a/packages/chakra-ui-core/src/InputGroup/index.js b/packages/chakra-ui-core/src/InputGroup/index.js index 149953fb..1e9b101d 100644 --- a/packages/chakra-ui-core/src/InputGroup/index.js +++ b/packages/chakra-ui-core/src/InputGroup/index.js @@ -1,75 +1,2 @@ -import { StringArray } from '../config/props/props.types' -import { baseProps } from '../config' -import { inputSizes } from '../Input/input.styles' -import Box from '../Box' -import { cloneVNode, forwardProps } from '../utils' -import { InputLeftElement, InputRightElement } from '../InputElement' -import Input from '../Input' - -const InputGroup = { - name: 'InputGroup', - inject: ['$theme'], - props: { - ...baseProps, - size: { - type: StringArray, - default: 'md' - } - }, - computed: { - theme () { - return this.$theme() - } - }, - render (h) { - const { sizes } = this.theme - let pl = null - let pr = null - const height = inputSizes[this.size] && inputSizes[this.size]['height'] - const children = this.$slots.default.filter(e => e.tag) - const clones = children - .map((vnode) => { - if (vnode.tag.includes(InputLeftElement.name)) { - pl = sizes[height] - } - if (vnode.tag.includes(InputRightElement.name)) { - pr = sizes[height] - } - if (vnode.tag.includes(Input.name)) { - const clone = cloneVNode(vnode, h) - return h(clone.componentOptions.Ctor, { - ...clone.data, - ...(clone.componentOptions.listeners || {}), - props: { - ...(clone.data.props || {}), - ...clone.componentOptions.propsData, - borderRadius: clone.componentOptions.propsData.rounded, - size: this.size, - paddingLeft: clone.componentOptions.propsData.pl || pl, - paddingRight: clone.componentOptions.propsData.pr || pr - } - }, vnode.componentOptions.children) - } - const clone = cloneVNode(vnode, h) - return h(clone.componentOptions.Ctor, { - ...clone.data, - ...(clone.componentOptions.listeners || {}), - props: { - ...(clone.data.props || {}), - ...clone.componentOptions.propsData, - size: this.size - } - }, vnode.componentOptions.children) - }) - - return h(Box, { - props: { - ...forwardProps(this.$props), - display: 'flex', - position: 'relative' - } - }, clones) - } -} - +import InputGroup from './InputGroup' export default InputGroup diff --git a/packages/chakra-ui-core/src/Link/Link.js b/packages/chakra-ui-core/src/Link/Link.js new file mode 100644 index 00000000..9624c750 --- /dev/null +++ b/packages/chakra-ui-core/src/Link/Link.js @@ -0,0 +1,64 @@ +import styleProps from '../config/props' +import PseudoBox from '../PseudoBox' +import { forwardProps, kebabify } from '../utils' +import { SNA } from '../config/props/props.types' + +/** + * Issue: + * - Text decoration on hover not working. Problem source could be with styled components internally + */ + +const Link = { + name: 'Link', + props: { + as: { + type: String, + default: 'a' + }, + to: SNA, + isDisabled: Boolean, + isExternal: Boolean, + ...styleProps + }, + computed: { + isRouterLink () { + return ['router-link', 'nuxt-link'].includes(kebabify(this.as)) + } + }, + render (h) { + const externalAttrs = this.isExternal + ? { target: '_blank', rel: 'noopener noreferrer' } + : null + + return h(PseudoBox, { + props: { + as: this.as, + ...this.isRouterLink && { to: this.to }, + transition: `all 0.15s ease-out`, + cursor: 'pointer', + textDecoration: 'none', + outline: 'none', + _focus: { + boxShadow: 'outline' + }, + _hover: { textDecoration: 'underline' }, + _disabled: { + opacity: '0.4', + cursor: 'not-allowed', + textDecoration: 'none' + }, + ...forwardProps(this.$props) + }, + attrs: { + tabIndex: this.isDisabled ? -1 : undefined, + 'aria-disabled': this.isDisabled, + ...externalAttrs + }, + on: { + click: (e) => this.$emit('click', e) + } + }, this.$slots.default) + } +} + +export default Link diff --git a/packages/chakra-ui-core/src/Link/index.js b/packages/chakra-ui-core/src/Link/index.js index 9624c750..db48dd8d 100644 --- a/packages/chakra-ui-core/src/Link/index.js +++ b/packages/chakra-ui-core/src/Link/index.js @@ -1,64 +1,2 @@ -import styleProps from '../config/props' -import PseudoBox from '../PseudoBox' -import { forwardProps, kebabify } from '../utils' -import { SNA } from '../config/props/props.types' - -/** - * Issue: - * - Text decoration on hover not working. Problem source could be with styled components internally - */ - -const Link = { - name: 'Link', - props: { - as: { - type: String, - default: 'a' - }, - to: SNA, - isDisabled: Boolean, - isExternal: Boolean, - ...styleProps - }, - computed: { - isRouterLink () { - return ['router-link', 'nuxt-link'].includes(kebabify(this.as)) - } - }, - render (h) { - const externalAttrs = this.isExternal - ? { target: '_blank', rel: 'noopener noreferrer' } - : null - - return h(PseudoBox, { - props: { - as: this.as, - ...this.isRouterLink && { to: this.to }, - transition: `all 0.15s ease-out`, - cursor: 'pointer', - textDecoration: 'none', - outline: 'none', - _focus: { - boxShadow: 'outline' - }, - _hover: { textDecoration: 'underline' }, - _disabled: { - opacity: '0.4', - cursor: 'not-allowed', - textDecoration: 'none' - }, - ...forwardProps(this.$props) - }, - attrs: { - tabIndex: this.isDisabled ? -1 : undefined, - 'aria-disabled': this.isDisabled, - ...externalAttrs - }, - on: { - click: (e) => this.$emit('click', e) - } - }, this.$slots.default) - } -} - +import Link from './Link' export default Link diff --git a/packages/chakra-ui-core/src/List/List.js b/packages/chakra-ui-core/src/List/List.js new file mode 100644 index 00000000..ea8fff8a --- /dev/null +++ b/packages/chakra-ui-core/src/List/List.js @@ -0,0 +1,92 @@ +import { baseProps } from '../config' +import Box from '../Box' +import PseudoBox from '../PseudoBox' +import Icon from '../Icon' +import { cleanChildren, isDef, cloneVNodeElement, forwardProps } from '../utils' +import { SNA } from '../config/props/props.types' +import styleProps from '../config/props' + +const List = { + name: 'List', + props: { + ...baseProps, + styleType: { + type: String, + default: 'none' + }, + stylePos: { + type: String, + default: 'inside' + }, + spacing: SNA + }, + render (h) { + const children = this.$slots.default + if (!isDef(children)) { + console.error('[Chakra-ui: List]: List component expects at east one child') + return null + } + const validChildren = cleanChildren(children) + + const clones = validChildren.map((vnode, index) => { + const isLast = index + 1 === validChildren.length + if (isLast) { + return vnode + } + + return cloneVNodeElement(vnode, { + props: { + spacing: this.spacing + } + }, h) + }) + + return h(Box, { + props: { + as: 'ul', + listStyleType: this.styleType, + listStylePosition: this.stylePos, + ...forwardProps(this.$props) + } + }, clones) + } +} + +const ListItem = { + name: 'ListItem', + props: { + ...styleProps, + spacing: SNA + }, + render (h) { + return h(PseudoBox, { + props: { + as: 'li', + mb: this.spacing + } + }, this.$slots.default) + } +} + +const ListIcon = { + name: 'ListIcon', + props: { + ...baseProps, + icon: String + }, + render (h) { + return h(Icon, { + props: { + name: this.icon, + mr: 2, + ...forwardProps(this.$props) + } + }) + } +} + +export default List +export { + ListItem, + ListIcon +} diff --git a/packages/chakra-ui-core/src/List/index.js b/packages/chakra-ui-core/src/List/index.js index ea8fff8a..69bfe31e 100644 --- a/packages/chakra-ui-core/src/List/index.js +++ b/packages/chakra-ui-core/src/List/index.js @@ -1,92 +1,3 @@ -import { baseProps } from '../config' -import Box from '../Box' -import PseudoBox from '../PseudoBox' -import Icon from '../Icon' -import { cleanChildren, isDef, cloneVNodeElement, forwardProps } from '../utils' -import { SNA } from '../config/props/props.types' -import styleProps from '../config/props' - -const List = { - name: 'List', - props: { - ...baseProps, - styleType: { - type: String, - default: 'none' - }, - stylePos: { - type: String, - default: 'inside' - }, - spacing: SNA - }, - render (h) { - const children = this.$slots.default - if (!isDef(children)) { - console.error('[Chakra-ui: List]: List component expects at east one child') - return null - } - const validChildren = cleanChildren(children) - - const clones = validChildren.map((vnode, index) => { - const isLast = index + 1 === validChildren.length - if (isLast) { - return vnode - } - - return cloneVNodeElement(vnode, { - props: { - spacing: this.spacing - } - }, h) - }) - - return h(Box, { - props: { - as: 'ul', - listStyleType: this.styleType, - listStylePosition: this.stylePos, - ...forwardProps(this.$props) - } - }, clones) - } -} - -const ListItem = { - name: 'ListItem', - props: { - ...styleProps, - spacing: SNA - }, - render (h) { - return h(PseudoBox, { - props: { - as: 'li', - mb: this.spacing - } - }, this.$slots.default) - } -} - -const ListIcon = { - name: 'ListIcon', - props: { - ...baseProps, - icon: String - }, - render (h) { - return h(Icon, { - props: { - name: this.icon, - mr: 2, - ...forwardProps(this.$props) - } - }) - } -} - +import List from './List' export default List -export { - ListItem, - ListIcon -} +export * from './List' diff --git a/packages/chakra-ui-core/src/Menu/Menu.js b/packages/chakra-ui-core/src/Menu/Menu.js new file mode 100644 index 00000000..17d9021a --- /dev/null +++ b/packages/chakra-ui-core/src/Menu/Menu.js @@ -0,0 +1,516 @@ +// eslint-disable-next-line +import { useId, getFocusables, canUseDOM, forwardProps } from '../utils' +import styleProps, { baseProps } from '../config/props' +import Button from '../Button' +import { buttonProps } from '../Button/button.props' +import { useMenuListStyle, useMenuItemStyle } from './menu.styles' +import { Popper } from '../Popper' +import Text from '../Text' +import PseudoBox from '../PseudoBox' +import Fragment from '../Fragment' +import Divider from '../Divider' +import Box from '../Box' + +const menuProps = { + controlledIsOpen: Boolean, + isControlled: Boolean, + defaultIsOpen: Boolean, + onOpen: Function, + onClose: Function, + autoSelect: { + type: Boolean, + default: true + }, + closeOnBlur: { + type: Boolean, + default: true + }, + closeOnSelect: { + type: Boolean, + default: true + }, + defaultActiveIndex: Number, + placement: String, + ...baseProps +} + +const Menu = { + name: 'Menu', + inject: ['$colorMode', '$theme'], + provide () { + return { + $MenuContext: () => this.MenuContext + } + }, + computed: { + colorMode () { + return this.$colorMode() + }, + theme () { + return this.$theme() + }, + menuId () { + return `menu-${useId()}` + }, + buttonId () { + return `menubutton-${useId()}` + }, + MenuContext () { + return { + activeIndex: this.activeIndex, + isOpen: this.isOpen, + menuNode: this.menuNode, + buttonNode: this.buttonNode, + focusableItems: this.focusableItems, + placement: this.placement, + menuId: this.menuId, + buttonId: this.buttonId, + colorMode: this.colorMode, + focusAtIndex: this.focusAtIndex, + focusOnLastItem: this.focusOnLastItem, + focusOnFirstItem: this.focusOnFirstItem, + closeMenu: this.closeMenu, + autoSelect: this.autoSelect, + closeOnSelect: this.closeOnSelect, + closeOnBlur: this.closeOnBlur + } + } + }, + props: menuProps, + data () { + return { + isOpen: this.isControlled ? this.controlledIsOpen : this.defaultIsOpen || false, + activeIndex: this.defaultActiveIndex || -1, + focusableItems: null, + menuNode: undefined, + buttonNode: undefined, + prevIsOpen: undefined + } + }, + mounted () { + let menuNode + let buttonNode + this.$nextTick(() => { + // In child components bind menuId to menuNode and bind it + menuNode = canUseDOM && document.querySelector(`#${this.menuId}`) + this.menuNode = menuNode + + buttonNode = canUseDOM && document.querySelector(`#${this.buttonId}`) + this.buttonNode = buttonNode + }) + + this.$watch('isOpen', (_newVal, oldVal) => { + this.prevIsOpen = oldVal + }, { + immediate: true + }) + + this.$watch('isOpen', (isOpen) => { + if (isOpen && menuNode) { + let focusables = getFocusables(menuNode).filter(node => + ['menuitem', 'menuitemradio', 'menuitemcheckbox'].includes( + node.getAttribute('role') + ) + ) + this.focusableItems = menuNode ? focusables : [] + this.initTabIndex() + } + }) + + this.$watch(vm => [vm.activeIndex, vm.isOpen, vm.menuNode, vm.buttonNode], () => { + if (this.activeIndex !== -1) { + this.focusableItems[this.activeIndex] && + this.focusableItems[this.activeIndex].focus() + this.updateTabIndex(this.activeIndex) + } + if (this.activeIndex === -1 && !this.isOpen && this.prevIsOpen) { + this.buttonNode && this.buttonNode.focus() + } + if (this.activeIndex === -1 && this.isOpen) { + this.menuNode && this.menuNode.focus() + } + }) + }, + methods: { + /** + * Initializes tab indexing on menu list items + */ + initTabIndex () { + this.focusableItems.forEach( + (node, index) => index === 0 && node.setAttribute('tabindex', 0) + ) + }, + /** + * Updates tab index for menulist items + * @param {Number} index Position index of menu list item + */ + updateTabIndex (index) { + if (this.focusableItems.length > 0) { + let nodeAtIndex = this.focusableItems[index] + this.focusableItems.forEach(node => { + if (node !== nodeAtIndex) { + node.setAttribute('tabindex', -1) + } + }) + nodeAtIndex.setAttribute('tabindex', 0) + } + }, + /** + * Resets tab index of menu list items + */ + resetTabIndex () { + if (this.focusableItems) { + this.focusableItems.forEach(node => node.setAttribute('tabindex', -1)) + } + }, + /** + * Opens Menu component + */ + openMenu () { + if (!this.isControlled) { + this.isOpen = true + } + + if (this.onOpen) { + this.onOpen() + } + }, + /** + * Focuses first menulist element. + */ + focusOnFirstItem () { + this.openMenu() + this.activeIndex = 0 + }, + /** + * Focuses an element at a particular index + * @param {Number} index Menulist items index + */ + focusAtIndex (index) { + this.activeIndex = index + }, + /** + * Focuses last menulist item + */ + focusOnLastItem () { + this.openMenu() + this.activeIndex = this.focusableItems.length - 1 + }, + /** + * Closes Menu + */ + closeMenu () { + if (!this.isControlled) { + this.isOpen = false + } + + if (this.onClose) { + this.onClose() + } + + this.activeIndex = -1 + this.resetTabIndex() + } + }, + render (h) { + return h(Fragment, [ + this.$scopedSlots.default({ + isOpen: this.isOpen + }) + ]) + } +} + +const MenuButton = { + name: 'MenuButton', + inject: ['$MenuContext'], + props: { + ...buttonProps, + ...styleProps + }, + computed: { + context () { + return this.$MenuContext() + } + }, + render (h) { + const { isOpen, buttonId, menuId, closeMenu, autoSelect, focusOnFirstItem, focusOnLastItem, openMenu } = this.context + return h(Button, { + props: { + ...forwardProps(this.$props), + isLoading: false + }, + attrs: { + id: buttonId, + role: 'button', + 'aria-haspopup': 'menu', + 'aria-expanded': isOpen, + 'aria-controls': menuId + }, + nativeOn: { + click: (event) => { + this.$emit('click', event) + if (isOpen) { + closeMenu() + } else { + if (autoSelect) { + focusOnFirstItem() + } else { + openMenu() + } + } + }, + keydown: (event) => { + if (event.key === 'ArrowDown') { + event.preventDefault() + focusOnFirstItem() + } + + if (event.key === 'ArrowUp') { + event.preventDefault() + focusOnLastItem() + } + } + } + }, this.$slots.default) + } +} + +const MenuList = { + name: 'MenuList', + props: styleProps, + inject: ['$MenuContext', '$colorMode'], + computed: { + context () { + return this.$MenuContext() + }, + menuListStyles () { + return colorMode => useMenuListStyle(colorMode) + }, + colorMode () { + return this.$colorMode() + } + }, + methods: { + handleKeyDown (event) { + const { activeIndex: index, focusAtIndex, focusOnFirstItem, focusOnLastItem, closeMenu, focusableItems } = this.context + const count = focusableItems.length + let nextIndex + if (event.key === 'ArrowDown') { + event.preventDefault() + nextIndex = (index + 1) % count + focusAtIndex(nextIndex) + } else if (event.key === 'ArrowUp') { + event.preventDefault() + nextIndex = (index - 1 + count) % count + focusAtIndex(nextIndex) + } else if (event.key === 'Home') { + focusOnFirstItem() + } else if (event.key === 'End') { + focusOnLastItem() + } else if (event.key === 'Tab') { + event.preventDefault() + } else if (event.key === 'Escape') { + closeMenu() + } + + // Set focus based on first character + if (/^[a-z0-9_-]$/i.test(event.key)) { + event.stopPropagation() + event.preventDefault() + let foundNode = focusableItems.find(item => + item.textContent.toLowerCase().startsWith(event.key) + ) + if (foundNode) { + nextIndex = focusableItems.indexOf(foundNode) + focusAtIndex(nextIndex) + } + } + + this.$emit('keydown', event) + }, + handleBlur (event) { + const { menuNode, buttonNode, isOpen, closeOnBlur, closeMenu } = this.context + if ( + closeOnBlur && + isOpen && + menuNode && + buttonNode && + !menuNode.contains(event.relatedTarget) && + !buttonNode.contains(event.relatedTarget) + ) { + closeMenu() + } + + this.$emit('blur', event) + } + }, + render (h) { + const { isOpen, buttonNode, menuId, buttonId, placement } = this.context + return h(Popper, { + props: { + usePortal: false, + isOpen, + anchorEl: buttonNode, + placement, + modifiers: { + preventOverflow: { enabled: true, boundariesElement: 'viewport' } + }, + closeOnClickAway: true, + minW: '3xs', + rounded: 'md', + py: 2, + 'z-index': 1, + _focus: { + outline: 0 + }, + ...this.menuListStyles(this.colorMode), + ...forwardProps(this.$props) + }, + attrs: { + id: menuId, + role: 'menu', + 'aria-labelledby': buttonId, + tabIndex: -1 + }, + nativeOn: { + keydown: this.handleKeyDown, + blur: this.handleBlur + } + }, this.$slots.default) + } +} + +const MenuItem = { + name: 'MenuItem', + inject: ['$MenuContext', '$theme', '$colorMode'], + props: { + ...styleProps, + isDisabled: Boolean, + role: { + type: String, + default: 'menuitem' + } + }, + computed: { + context () { + return this.$MenuContext() + }, + menuItemStyles () { + return props => useMenuItemStyle(props) + }, + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + } + }, + render (h) { + const { focusableItems, focusAtIndex, closeOnSelect, closeMenu } = this.context + return h(PseudoBox, { + props: { + as: 'button', + display: 'flex', + textDecoration: 'none', + color: 'inherit', + minHeight: '32px', + alignItems: 'center', + textAlign: 'left', + outline: 'none', + px: 4, + ...this.menuItemStyles({ theme: this.theme, colorMode: this.colorMode }), + ...forwardProps(this.$props) + }, + attrs: { + role: this.role, + tabIndex: -1, + disabled: this.isDisabled, + 'aria-disabled': this.isDisabled + }, + nativeOn: { + click: event => { + this.$emit('click', event) + if (this.isDisabled) { + event.stopPropagation() + event.preventDefault() + return + } + if (closeOnSelect) { + closeMenu() + } + }, + mouseenter: event => { + this.$emit('mouseenter', event) + if (this.isDisabled) { + event.stopPropagation() + event.preventDefault() + return + } + if (focusableItems && focusableItems.length > 0) { + let nextIndex = focusableItems.indexOf(event.currentTarget) + focusAtIndex(nextIndex) + } + }, + mouseleave: () => { + focusAtIndex(-1) + }, + keydown: event => { + this.$emit('keydown', event) + if (this.isDisabled) return + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + + // We also emit click event to simulate click event for keyboard "Enter" keydown event + this.$emit('click') + + if (closeOnSelect) { + closeMenu() + } + } + } + } + }, this.$slots.default) + } +} + +const MenuDivider = { + name: 'MenuDivider', + props: baseProps, + render (h) { + return h(Divider, { + props: { + marginTop: '0.5rem', + marginBottom: '0.5rem', + ...forwardProps(this.$props) + } + }) + } +} + +const MenuGroup = { + name: 'MenuGroup', + props: { + title: String, + ...baseProps + }, + render (h) { + return h(Box, { + attrs: { role: 'group' } + }, [ + this.title && h(Text, { + props: { mx: 4, my: 2, fontWeight: 'semibold', fontSize: 'sm', ...forwardProps(this.$props) } + }, this.title), + this.$slots.default + ]) + } +} + +export { + Menu, + MenuButton, + MenuList, + MenuItem, + MenuGroup, + MenuDivider +} diff --git a/packages/chakra-ui-core/src/Menu/index.js b/packages/chakra-ui-core/src/Menu/index.js index 4430635f..67984a1e 100644 --- a/packages/chakra-ui-core/src/Menu/index.js +++ b/packages/chakra-ui-core/src/Menu/index.js @@ -1,516 +1 @@ -// eslint-disable-next-line -import { useId, getFocusables, canUseDOM, forwardProps } from '../utils' -import styleProps, { baseProps } from '../config/props' -import Button from '../Button' -import { buttonProps } from '../Button/button.props' -import { useMenuListStyle, useMenuItemStyle } from './menu.styles' -import { Popper } from '../Popper' -import Text from '../Text' -import PseudoBox from '../PseudoBox' -import Fragment from '../Fragment' -import Divider from '../Divider' -import Box from '../Box' - -const menuProps = { - controlledIsOpen: Boolean, - isControlled: Boolean, - defaultIsOpen: Boolean, - onOpen: Function, - onClose: Function, - autoSelect: { - type: Boolean, - default: true - }, - closeOnBlur: { - type: Boolean, - default: true - }, - closeOnSelect: { - type: Boolean, - default: true - }, - defaultActiveIndex: Number, - placement: String, - ...baseProps -} - -const Menu = { - name: 'Menu', - inject: ['$colorMode', '$theme'], - provide () { - return { - $MenuContext: () => this.MenuContext - } - }, - computed: { - colorMode () { - return this.$colorMode() - }, - theme () { - return this.$theme() - }, - menuId () { - return `menu-${useId()}` - }, - buttonId () { - return `menubutton-${useId()}` - }, - MenuContext () { - return { - activeIndex: this.activeIndex, - isOpen: this.isOpen, - menuNode: this.menuNode, - buttonNode: this.buttonNode, - focusableItems: this.focusableItems, - placement: this.placement, - menuId: this.menuId, - buttonId: this.buttonId, - colorMode: this.colorMode, - focusAtIndex: this.focusAtIndex, - focusOnLastItem: this.focusOnLastItem, - focusOnFirstItem: this.focusOnFirstItem, - closeMenu: this.closeMenu, - autoSelect: this.autoSelect, - closeOnSelect: this.closeOnSelect, - closeOnBlur: this.closeOnBlur - } - } - }, - props: menuProps, - data () { - return { - isOpen: this.isControlled ? this.controlledIsOpen : this.defaultIsOpen || false, - activeIndex: this.defaultActiveIndex || -1, - focusableItems: null, - menuNode: undefined, - buttonNode: undefined, - prevIsOpen: undefined - } - }, - mounted () { - let menuNode - let buttonNode - this.$nextTick(() => { - // In child components bind menuId to menuNode and bind it - menuNode = canUseDOM && document.querySelector(`#${this.menuId}`) - this.menuNode = menuNode - - buttonNode = canUseDOM && document.querySelector(`#${this.buttonId}`) - this.buttonNode = buttonNode - }) - - this.$watch('isOpen', (_newVal, oldVal) => { - this.prevIsOpen = oldVal - }, { - immediate: true - }) - - this.$watch('isOpen', (isOpen) => { - if (isOpen && menuNode) { - let focusables = getFocusables(menuNode).filter(node => - ['menuitem', 'menuitemradio', 'menuitemcheckbox'].includes( - node.getAttribute('role') - ) - ) - this.focusableItems = menuNode ? focusables : [] - this.initTabIndex() - } - }) - - this.$watch(vm => [vm.activeIndex, vm.isOpen, vm.menuNode, vm.buttonNode], () => { - if (this.activeIndex !== -1) { - this.focusableItems[this.activeIndex] && - this.focusableItems[this.activeIndex].focus() - this.updateTabIndex(this.activeIndex) - } - if (this.activeIndex === -1 && !this.isOpen && this.prevIsOpen) { - this.buttonNode && this.buttonNode.focus() - } - if (this.activeIndex === -1 && this.isOpen) { - this.menuNode && this.menuNode.focus() - } - }) - }, - methods: { - /** - * Initializes tab indexing on menu list items - */ - initTabIndex () { - this.focusableItems.forEach( - (node, index) => index === 0 && node.setAttribute('tabindex', 0) - ) - }, - /** - * Updates tab index for menulist items - * @param {Number} index Position index of menu list item - */ - updateTabIndex (index) { - if (this.focusableItems.length > 0) { - let nodeAtIndex = this.focusableItems[index] - this.focusableItems.forEach(node => { - if (node !== nodeAtIndex) { - node.setAttribute('tabindex', -1) - } - }) - nodeAtIndex.setAttribute('tabindex', 0) - } - }, - /** - * Resets tab index of menu list items - */ - resetTabIndex () { - if (this.focusableItems) { - this.focusableItems.forEach(node => node.setAttribute('tabindex', -1)) - } - }, - /** - * Opens Menu component - */ - openMenu () { - if (!this.isControlled) { - this.isOpen = true - } - - if (this.onOpen) { - this.onOpen() - } - }, - /** - * Focuses first menulist element. - */ - focusOnFirstItem () { - this.openMenu() - this.activeIndex = 0 - }, - /** - * Focuses an element at a particular index - * @param {Number} index Menulist items index - */ - focusAtIndex (index) { - this.activeIndex = index - }, - /** - * Focuses last menulist item - */ - focusOnLastItem () { - this.openMenu() - this.activeIndex = this.focusableItems.length - 1 - }, - /** - * Closes Menu - */ - closeMenu () { - if (!this.isControlled) { - this.isOpen = false - } - - if (this.onClose) { - this.onClose() - } - - this.activeIndex = -1 - this.resetTabIndex() - } - }, - render (h) { - return h(Fragment, [ - this.$scopedSlots.default({ - isOpen: this.isOpen - }) - ]) - } -} - -const MenuButton = { - name: 'MenuButton', - inject: ['$MenuContext'], - props: { - ...buttonProps, - ...styleProps - }, - computed: { - context () { - return this.$MenuContext() - } - }, - render (h) { - const { isOpen, buttonId, menuId, closeMenu, autoSelect, focusOnFirstItem, focusOnLastItem, openMenu } = this.context - return h(Button, { - props: { - ...forwardProps(this.$props), - isLoading: false - }, - attrs: { - id: buttonId, - role: 'button', - 'aria-haspopup': 'menu', - 'aria-expanded': isOpen, - 'aria-controls': menuId - }, - nativeOn: { - click: (event) => { - this.$emit('click', event) - if (isOpen) { - closeMenu() - } else { - if (autoSelect) { - focusOnFirstItem() - } else { - openMenu() - } - } - }, - keydown: (event) => { - if (event.key === 'ArrowDown') { - event.preventDefault() - focusOnFirstItem() - } - - if (event.key === 'ArrowUp') { - event.preventDefault() - focusOnLastItem() - } - } - } - }, this.$slots.default) - } -} - -const MenuList = { - name: 'MenuList', - props: styleProps, - inject: ['$MenuContext', '$colorMode'], - computed: { - context () { - return this.$MenuContext() - }, - menuListStyles () { - return colorMode => useMenuListStyle(colorMode) - }, - colorMode () { - return this.$colorMode() - } - }, - methods: { - handleKeyDown (event) { - const { activeIndex: index, focusAtIndex, focusOnFirstItem, focusOnLastItem, closeMenu, focusableItems } = this.context - const count = focusableItems.length - let nextIndex - if (event.key === 'ArrowDown') { - event.preventDefault() - nextIndex = (index + 1) % count - focusAtIndex(nextIndex) - } else if (event.key === 'ArrowUp') { - event.preventDefault() - nextIndex = (index - 1 + count) % count - focusAtIndex(nextIndex) - } else if (event.key === 'Home') { - focusOnFirstItem() - } else if (event.key === 'End') { - focusOnLastItem() - } else if (event.key === 'Tab') { - event.preventDefault() - } else if (event.key === 'Escape') { - closeMenu() - } - - // Set focus based on first character - if (/^[a-z0-9_-]$/i.test(event.key)) { - event.stopPropagation() - event.preventDefault() - let foundNode = focusableItems.find(item => - item.textContent.toLowerCase().startsWith(event.key) - ) - if (foundNode) { - nextIndex = focusableItems.indexOf(foundNode) - focusAtIndex(nextIndex) - } - } - - this.$emit('keydown', event) - }, - handleBlur (event) { - const { menuNode, buttonNode, isOpen, closeOnBlur, closeMenu } = this.context - if ( - closeOnBlur && - isOpen && - menuNode && - buttonNode && - !menuNode.contains(event.relatedTarget) && - !buttonNode.contains(event.relatedTarget) - ) { - closeMenu() - } - - this.$emit('blur', event) - } - }, - render (h) { - const { isOpen, buttonNode, menuId, buttonId, placement } = this.context - return h(Popper, { - props: { - usePortal: false, - isOpen, - anchorEl: buttonNode, - placement, - modifiers: { - preventOverflow: { enabled: true, boundariesElement: 'viewport' } - }, - closeOnClickAway: true, - minW: '3xs', - rounded: 'md', - py: 2, - 'z-index': 1, - _focus: { - outline: 0 - }, - ...this.menuListStyles(this.colorMode), - ...forwardProps(this.$props) - }, - attrs: { - id: menuId, - role: 'menu', - 'aria-labelledby': buttonId, - tabIndex: -1 - }, - nativeOn: { - keydown: this.handleKeyDown, - blur: this.handleBlur - } - }, this.$slots.default) - } -} - -const MenuItem = { - name: 'MenuItem', - inject: ['$MenuContext', '$theme', '$colorMode'], - props: { - ...styleProps, - isDisabled: Boolean, - role: { - type: String, - default: 'menuitem' - } - }, - computed: { - context () { - return this.$MenuContext() - }, - menuItemStyles () { - return props => useMenuItemStyle(props) - }, - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - } - }, - render (h) { - const { focusableItems, focusAtIndex, closeOnSelect, closeMenu } = this.context - return h(PseudoBox, { - props: { - as: 'button', - display: 'flex', - textDecoration: 'none', - color: 'inherit', - minHeight: '32px', - alignItems: 'center', - textAlign: 'left', - outline: 'none', - px: 4, - ...this.menuItemStyles({ theme: this.theme, colorMode: this.colorMode }), - ...forwardProps(this.$props) - }, - attrs: { - role: this.role, - tabIndex: -1, - disabled: this.isDisabled, - 'aria-disabled': this.isDisabled - }, - nativeOn: { - click: event => { - this.$emit('click', event) - if (this.isDisabled) { - event.stopPropagation() - event.preventDefault() - return - } - if (closeOnSelect) { - closeMenu() - } - }, - mouseenter: event => { - this.$emit('mouseenter', event) - if (this.isDisabled) { - event.stopPropagation() - event.preventDefault() - return - } - if (focusableItems && focusableItems.length > 0) { - let nextIndex = focusableItems.indexOf(event.currentTarget) - focusAtIndex(nextIndex) - } - }, - mouseleave: () => { - focusAtIndex(-1) - }, - keydown: event => { - this.$emit('keydown', event) - if (this.isDisabled) return - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault() - - // We also emit click event to simulate click event for keyboard "Enter" keydown event - this.$emit('click') - - if (closeOnSelect) { - closeMenu() - } - } - } - } - }, this.$slots.default) - } -} - -const MenuDivider = { - name: 'MenuDivider', - props: baseProps, - render (h) { - return h(Divider, { - props: { - marginTop: '0.5rem', - marginBottom: '0.5rem', - ...forwardProps(this.$props) - } - }) - } -} - -const MenuGroup = { - name: 'MenuGroup', - props: { - title: String, - ...baseProps - }, - render (h) { - return h(Box, { - attrs: { role: 'group' } - }, [ - this.title && h(Text, { - props: { mx: '1', my: 'px', fontWeight: 'semibold', fontSize: 'sm', ...forwardProps(this.$props) } - }, this.title), - this.$slots.default - ]) - } -} - -export { - Menu, - MenuButton, - MenuList, - MenuItem, - MenuGroup, - MenuDivider -} +export * from './Menu' diff --git a/packages/chakra-ui-core/src/Modal/Modal.js b/packages/chakra-ui-core/src/Modal/Modal.js new file mode 100644 index 00000000..03857b2b --- /dev/null +++ b/packages/chakra-ui-core/src/Modal/Modal.js @@ -0,0 +1,540 @@ +import props from './modal.props' +import { useId, canUseDOM, getElById, isVueComponent, getElement, getFocusables, cleanChildren, forwardProps, wrapEvent } from '../utils' +import Portal from '../Portal' +import PseudoBox from '../PseudoBox' +import { Fade } from '../Transition' +import { hideOthers } from 'aria-hidden' +import { FocusTrap } from 'focus-trap-vue' +import { baseProps } from '../config' +import Box from '../Box' +import styleProps from '../config/props' +import CloseButton from '../CloseButton' +import isFunction from 'lodash-es/isFunction' + +const Modal = { + name: 'Modal', + props, + data () { + return { + addAriaLabelledby: false, + addAriaDescribedby: false, + modalNode: undefined, + contentNode: undefined + } + }, + provide () { + return { + $ModalContext: () => this.ModalContext + } + }, + computed: { + _id () { + return this.id || useId(4) + }, + contentId () { + return this.formatIds(this._id)['content'] + }, + headerId () { + return this.formatIds(this._id)['header'] + }, + bodyId () { + return this.formatIds(this._id)['body'] + }, + modalId () { + return `modal-${this._id}` + }, + portalTarget () { + return `#modal-portal-${this._id}` + }, + ModalContext () { + return { + isOpen: this.isOpen, + initialFocusRef: this.initialFocusRef, + onClose: this.onClose, + blockScrollOnMount: this.blockScrollOnMount, + closeOnEsc: this.closeOnEsc, + closeOnOverlayClick: this.closeOnOverlayClick, + returnFocusOnClose: this.returnFocusOnClose, + contentNode: this.contentNode, + scrollBehavior: this.scrollBehavior, + isCentered: this.isCentered, + size: this.size, + headerId: this.headerId, + bodyId: this.bodyId, + contentId: this.contentId, + addAriaLabelledby: this.addAriaLabelledby, + addAriaDescribedby: this.addAriaDescribedby + } + } + }, + mounted () { + if (typeof this.addAriaLabels === 'object') { + this.addAriaLabelledby = this.addAriaLabels['header'] + this.addAriaDescribedby = this.addAriaLabels['body'] + } + + if (typeof this.addAriaLabels === 'boolean') { + this.addAriaLabelledby = this.addAriaLabels + this.addAriaDescribedby = this.addAriaLabels + } + + this.$nextTick(() => { + this.modalNode = getElById(this.modalId) + }) + + /** + * Escape key press event handler for modal + * @param {Event} event Keyboard Event + */ + const handler = event => { + if (event.key === 'Escape' && this.closeOnEsc) { + this.onClose(event, 'pressedEscape') + } + } + + this.$watch(vm => [vm.isOpen, vm.blockScrollOnMount], () => { + if (this.isOpen && !this.closeOnOverlayClick) { + canUseDOM && document.addEventListener('keydown', handler) + } else { + document.addEventListener('keydown', handler) + } + }) + + this.$watch('isOpen', () => { + if (!this.isOpen) { + document.removeEventListener('keydown', handler) + } + }) + + let undoAriaHidden = null + this.$watch(vm => [vm.isOpen, vm.useInert], () => { + let mountNode = this.mountNode + if (this.isOpen && canUseDOM) { + if (this.useInert) { + undoAriaHidden = hideOthers(mountNode) + } + this.contentNode = getElById(this.contentId) + } else { + if (this.useInert && undoAriaHidden != null) { + undoAriaHidden() + } + } + }) + }, + methods: { + /** + * Handles focus trap activation + */ + activateFocusLock () { + setTimeout(() => { + if (this.initialFocusRef) { + const initialFocusRef = isFunction(this.initialFocusRef) ? this.getNode(this.initialFocusRef()) : this.getNode(this.initialFocusRef) + if (initialFocusRef) { + initialFocusRef.focus() + } + } else { + const contentNode = getElById(this.contentId) + if (contentNode) { + let focusables = getFocusables(contentNode) + if (focusables.length === 0) { + contentNode.focus() + } else { + const [el] = focusables + el && el.focus() + } + } + } + }) + }, + + /** + * Handles focus trap deactivation + */ + deactivateFocusLock () { + setTimeout(() => { + if (this.finalFocusRef) { + const finalFocusRef = this.getNode(this.finalFocusRef) + if (finalFocusRef) { + canUseDOM && finalFocusRef.focus() + } else { + console.warn(`[ChakraUI Modal]: Unable to locate final focus node "${this.finalFocusRef}".`) + } + } + }) + }, + + /** + * Gets the HTML element for a component or selector + * @param {Object|String} element Element or selector + */ + getNode (element) { + if (typeof element === 'object') { + const isVue = isVueComponent(element) + return isVue ? element.$el : element + } else if (typeof element === 'string') { + return getElement(element) + } + return null + } + }, + render (h) { + const children = cleanChildren(this.$slots.default) + + return h(Portal, { + props: { + append: true, + target: this.portalTarget, + disabled: false, + slim: true, + unmountOnDestroy: true, + targetSlim: true + } + }, [ + h(FocusTrap, { + props: { + returnFocusOnDeactivate: this.returnFocusOnClose && !this.finalFocusRef, + active: this.isOpen + }, + on: { + activate: this.activateFocusLock, + deactivate: this.deactivateFocusLock + } + }, [ + h(PseudoBox, { + props: { + position: 'relative' + }, + directives: [{ + name: 'scroll-lock', + value: this.isOpen && this.blockScrollOnMount + }] + }, [ + h(Fade, { + props: { + enterEasing: 'easeInCubic', + leaveEasing: 'easeOutCubic' + } + }, this.isOpen && [h('div', { + style: { + position: 'fixed', + top: 0, + right: 0, + bottom: 0, + left: 0 + } + }, children)]) + ]) + ]) + ]) + } +} + +/** + * ModalOverlay component + */ +const ModalOverlay = { + name: 'ModalOverlay', + props: baseProps, + render (h) { + return h(Box, { + props: { + pos: 'fixed', + bg: 'rgba(0,0,0,0.4)', + left: '0', + top: '0', + w: '100vw', + h: '100vh', + zIndex: 'overlay', + ...forwardProps(this.$props) + } + }) + } +} + +const ModalContent = { + name: 'ModalContent', + inheritAttrs: false, + inject: ['$ModalContext', '$colorMode'], + props: { + ...baseProps, + noStyles: Boolean, + zIndex: { + type: String, + default: 'modal' + } + }, + data () { + return { + colorModeStyles: { + light: { + bg: 'white', + shadow: '0 7px 14px 0 rgba(0,0,0, 0.1), 0 3px 6px 0 rgba(0, 0, 0, .07)' + }, + dark: { + bg: 'gray.700', + shadow: `rgba(0, 0, 0, 0.1) 0px 0px 0px 1px, rgba(0, 0, 0, 0.2) 0px 5px 10px, rgba(0, 0, 0, 0.4) 0px 15px 40px`, + color: 'whiteAlpha.900' + } + }, + wrapperStyle: {}, + contentStyle: {} + } + }, + computed: { + colorMode () { + return this.$colorMode() + }, + context () { + return this.$ModalContext() + }, + boxStyleProps () { + return this.colorModeStyles[this.colorMode] + } + }, + created () { + const { isCentered, scrollBehavior } = this.context + + let wrapperStyle = {} + let contentStyle = {} + + if (isCentered) { + wrapperStyle = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + } else { + contentStyle = { + top: '3.75rem', + mx: 'auto' + } + } + + if (scrollBehavior === 'inside') { + wrapperStyle = { + ...wrapperStyle, + maxHeight: 'calc(100vh - 7.5rem)', + overflow: 'hidden', + top: '3.75rem' + } + + contentStyle = { + ...contentStyle, + height: '100%', + top: 0 + } + } + + if (scrollBehavior === 'outside') { + wrapperStyle = { + ...wrapperStyle, + overflowY: 'auto', + overflowX: 'hidden' + } + + contentStyle = { + ...contentStyle, + my: '3.75rem', + top: 0 + } + } + + if (this.noStyles) { + wrapperStyle = {} + contentStyle = {} + } + + this.wrapperStyle = wrapperStyle + this.contentStyle = contentStyle + }, + render (h) { + const { + onClose, + bodyId, + headerId, + contentId, + size, + closeOnEsc, + addAriaLabelledby, + addAriaDescribedby, + closeOnOverlayClick + } = this.context + + return h(Box, { + props: { + ...forwardProps(this.$props), + ...this.wrapperStyle, + pos: 'fixed', + left: '0', + top: '0', + w: '100%', + h: '100%', + zIndex: this.zIndex || 'modal' + }, + nativeOn: { + click: (event) => { + event.stopPropagation() + if (closeOnOverlayClick) { + onClose && onClose(event, 'clickedOverlay') + this.$emit('clickedOverlay', event) + } + }, + keydown: (event) => { + if (event.key === 'Escape') { + event.stopPropagation() + if (closeOnEsc) { + onClose(event, 'pressedEscape') + } + } + } + } + }, [ + h(Box, { + props: { + as: 'section', + outline: 0, + maxWidth: size, + w: '100%', + pos: 'relative', + d: 'flex', + flexDir: 'column', + zIndex: this.zIndex, + fontFamily: 'body', + ...this.boxStyleProps, + ...this.contentStyle, + ...forwardProps(this.$props) + }, + attrs: { + role: 'dialog', + 'aria-modal': 'true', + tabIndex: -1, + id: contentId, + ...(addAriaDescribedby && { 'aria-describedby': bodyId }), + ...(addAriaLabelledby && { 'aria-labelledby': headerId }), + ...this.$attrs + }, + nativeOn: { + click: wrapEvent((e) => this.$emit('click', e), event => event.stopPropagation()) + } + }, this.$slots.default) + ]) + } +} + +const ModalHeader = { + name: 'ModalHeader', + inject: ['$ModalContext'], + props: baseProps, + computed: { + context () { + return this.$ModalContext() + } + }, + render (h) { + const { headerId } = this.context + + return h(Box, { + props: { + as: 'header', + px: 6, + py: 4, + position: 'relative', + fontSize: 'xl', + fontWeight: 'semibold', + ...forwardProps(this.$props) + }, + attrs: { + id: headerId + } + }, this.$slots.default) + } +} + +const ModalFooter = { + name: 'ModalFooter', + props: baseProps, + render (h) { + return h(Box, { + props: { + as: 'footer', + display: 'flex', + px: 6, + py: 4, + justifyContent: 'flex-end', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const ModalBody = { + name: 'ModalBody', + props: baseProps, + inject: ['$ModalContext'], + computed: { + context () { + return this.$ModalContext() + } + }, + render (h) { + const { bodyId, scrollBehavior } = this.context + + let style = {} + if (scrollBehavior === 'inside') { + style = { overflowY: 'auto' } + } + + return h(Box, { + props: { + px: 6, + py: 2, + flex: 1, + ...style, + ...forwardProps(this.$props) + }, + attrs: { + id: bodyId + } + }, this.$slots.default) + } +} + +const ModalCloseButton = { + name: 'ModalCloseButton', + props: styleProps, + inject: ['$ModalContext'], + computed: { + context () { + return this.$ModalContext() + } + }, + render (h) { + const { onClose } = this.context + return h(CloseButton, { + props: { + position: 'absolute', + top: '8px', + right: '12px', + ...forwardProps(this.$props) + }, + attrs: { + 'x-close-button': '' + }, + on: { + click: (e) => { + wrapEvent(onClose, event => this.$emit('click', event))(e) + } + } + }) + } +} + +export { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton +} diff --git a/packages/chakra-ui-core/src/Modal/index.js b/packages/chakra-ui-core/src/Modal/index.js index 03857b2b..8d3bcd7a 100644 --- a/packages/chakra-ui-core/src/Modal/index.js +++ b/packages/chakra-ui-core/src/Modal/index.js @@ -1,540 +1 @@ -import props from './modal.props' -import { useId, canUseDOM, getElById, isVueComponent, getElement, getFocusables, cleanChildren, forwardProps, wrapEvent } from '../utils' -import Portal from '../Portal' -import PseudoBox from '../PseudoBox' -import { Fade } from '../Transition' -import { hideOthers } from 'aria-hidden' -import { FocusTrap } from 'focus-trap-vue' -import { baseProps } from '../config' -import Box from '../Box' -import styleProps from '../config/props' -import CloseButton from '../CloseButton' -import isFunction from 'lodash-es/isFunction' - -const Modal = { - name: 'Modal', - props, - data () { - return { - addAriaLabelledby: false, - addAriaDescribedby: false, - modalNode: undefined, - contentNode: undefined - } - }, - provide () { - return { - $ModalContext: () => this.ModalContext - } - }, - computed: { - _id () { - return this.id || useId(4) - }, - contentId () { - return this.formatIds(this._id)['content'] - }, - headerId () { - return this.formatIds(this._id)['header'] - }, - bodyId () { - return this.formatIds(this._id)['body'] - }, - modalId () { - return `modal-${this._id}` - }, - portalTarget () { - return `#modal-portal-${this._id}` - }, - ModalContext () { - return { - isOpen: this.isOpen, - initialFocusRef: this.initialFocusRef, - onClose: this.onClose, - blockScrollOnMount: this.blockScrollOnMount, - closeOnEsc: this.closeOnEsc, - closeOnOverlayClick: this.closeOnOverlayClick, - returnFocusOnClose: this.returnFocusOnClose, - contentNode: this.contentNode, - scrollBehavior: this.scrollBehavior, - isCentered: this.isCentered, - size: this.size, - headerId: this.headerId, - bodyId: this.bodyId, - contentId: this.contentId, - addAriaLabelledby: this.addAriaLabelledby, - addAriaDescribedby: this.addAriaDescribedby - } - } - }, - mounted () { - if (typeof this.addAriaLabels === 'object') { - this.addAriaLabelledby = this.addAriaLabels['header'] - this.addAriaDescribedby = this.addAriaLabels['body'] - } - - if (typeof this.addAriaLabels === 'boolean') { - this.addAriaLabelledby = this.addAriaLabels - this.addAriaDescribedby = this.addAriaLabels - } - - this.$nextTick(() => { - this.modalNode = getElById(this.modalId) - }) - - /** - * Escape key press event handler for modal - * @param {Event} event Keyboard Event - */ - const handler = event => { - if (event.key === 'Escape' && this.closeOnEsc) { - this.onClose(event, 'pressedEscape') - } - } - - this.$watch(vm => [vm.isOpen, vm.blockScrollOnMount], () => { - if (this.isOpen && !this.closeOnOverlayClick) { - canUseDOM && document.addEventListener('keydown', handler) - } else { - document.addEventListener('keydown', handler) - } - }) - - this.$watch('isOpen', () => { - if (!this.isOpen) { - document.removeEventListener('keydown', handler) - } - }) - - let undoAriaHidden = null - this.$watch(vm => [vm.isOpen, vm.useInert], () => { - let mountNode = this.mountNode - if (this.isOpen && canUseDOM) { - if (this.useInert) { - undoAriaHidden = hideOthers(mountNode) - } - this.contentNode = getElById(this.contentId) - } else { - if (this.useInert && undoAriaHidden != null) { - undoAriaHidden() - } - } - }) - }, - methods: { - /** - * Handles focus trap activation - */ - activateFocusLock () { - setTimeout(() => { - if (this.initialFocusRef) { - const initialFocusRef = isFunction(this.initialFocusRef) ? this.getNode(this.initialFocusRef()) : this.getNode(this.initialFocusRef) - if (initialFocusRef) { - initialFocusRef.focus() - } - } else { - const contentNode = getElById(this.contentId) - if (contentNode) { - let focusables = getFocusables(contentNode) - if (focusables.length === 0) { - contentNode.focus() - } else { - const [el] = focusables - el && el.focus() - } - } - } - }) - }, - - /** - * Handles focus trap deactivation - */ - deactivateFocusLock () { - setTimeout(() => { - if (this.finalFocusRef) { - const finalFocusRef = this.getNode(this.finalFocusRef) - if (finalFocusRef) { - canUseDOM && finalFocusRef.focus() - } else { - console.warn(`[ChakraUI Modal]: Unable to locate final focus node "${this.finalFocusRef}".`) - } - } - }) - }, - - /** - * Gets the HTML element for a component or selector - * @param {Object|String} element Element or selector - */ - getNode (element) { - if (typeof element === 'object') { - const isVue = isVueComponent(element) - return isVue ? element.$el : element - } else if (typeof element === 'string') { - return getElement(element) - } - return null - } - }, - render (h) { - const children = cleanChildren(this.$slots.default) - - return h(Portal, { - props: { - append: true, - target: this.portalTarget, - disabled: false, - slim: true, - unmountOnDestroy: true, - targetSlim: true - } - }, [ - h(FocusTrap, { - props: { - returnFocusOnDeactivate: this.returnFocusOnClose && !this.finalFocusRef, - active: this.isOpen - }, - on: { - activate: this.activateFocusLock, - deactivate: this.deactivateFocusLock - } - }, [ - h(PseudoBox, { - props: { - position: 'relative' - }, - directives: [{ - name: 'scroll-lock', - value: this.isOpen && this.blockScrollOnMount - }] - }, [ - h(Fade, { - props: { - enterEasing: 'easeInCubic', - leaveEasing: 'easeOutCubic' - } - }, this.isOpen && [h('div', { - style: { - position: 'fixed', - top: 0, - right: 0, - bottom: 0, - left: 0 - } - }, children)]) - ]) - ]) - ]) - } -} - -/** - * ModalOverlay component - */ -const ModalOverlay = { - name: 'ModalOverlay', - props: baseProps, - render (h) { - return h(Box, { - props: { - pos: 'fixed', - bg: 'rgba(0,0,0,0.4)', - left: '0', - top: '0', - w: '100vw', - h: '100vh', - zIndex: 'overlay', - ...forwardProps(this.$props) - } - }) - } -} - -const ModalContent = { - name: 'ModalContent', - inheritAttrs: false, - inject: ['$ModalContext', '$colorMode'], - props: { - ...baseProps, - noStyles: Boolean, - zIndex: { - type: String, - default: 'modal' - } - }, - data () { - return { - colorModeStyles: { - light: { - bg: 'white', - shadow: '0 7px 14px 0 rgba(0,0,0, 0.1), 0 3px 6px 0 rgba(0, 0, 0, .07)' - }, - dark: { - bg: 'gray.700', - shadow: `rgba(0, 0, 0, 0.1) 0px 0px 0px 1px, rgba(0, 0, 0, 0.2) 0px 5px 10px, rgba(0, 0, 0, 0.4) 0px 15px 40px`, - color: 'whiteAlpha.900' - } - }, - wrapperStyle: {}, - contentStyle: {} - } - }, - computed: { - colorMode () { - return this.$colorMode() - }, - context () { - return this.$ModalContext() - }, - boxStyleProps () { - return this.colorModeStyles[this.colorMode] - } - }, - created () { - const { isCentered, scrollBehavior } = this.context - - let wrapperStyle = {} - let contentStyle = {} - - if (isCentered) { - wrapperStyle = { - display: 'flex', - alignItems: 'center', - justifyContent: 'center' - } - } else { - contentStyle = { - top: '3.75rem', - mx: 'auto' - } - } - - if (scrollBehavior === 'inside') { - wrapperStyle = { - ...wrapperStyle, - maxHeight: 'calc(100vh - 7.5rem)', - overflow: 'hidden', - top: '3.75rem' - } - - contentStyle = { - ...contentStyle, - height: '100%', - top: 0 - } - } - - if (scrollBehavior === 'outside') { - wrapperStyle = { - ...wrapperStyle, - overflowY: 'auto', - overflowX: 'hidden' - } - - contentStyle = { - ...contentStyle, - my: '3.75rem', - top: 0 - } - } - - if (this.noStyles) { - wrapperStyle = {} - contentStyle = {} - } - - this.wrapperStyle = wrapperStyle - this.contentStyle = contentStyle - }, - render (h) { - const { - onClose, - bodyId, - headerId, - contentId, - size, - closeOnEsc, - addAriaLabelledby, - addAriaDescribedby, - closeOnOverlayClick - } = this.context - - return h(Box, { - props: { - ...forwardProps(this.$props), - ...this.wrapperStyle, - pos: 'fixed', - left: '0', - top: '0', - w: '100%', - h: '100%', - zIndex: this.zIndex || 'modal' - }, - nativeOn: { - click: (event) => { - event.stopPropagation() - if (closeOnOverlayClick) { - onClose && onClose(event, 'clickedOverlay') - this.$emit('clickedOverlay', event) - } - }, - keydown: (event) => { - if (event.key === 'Escape') { - event.stopPropagation() - if (closeOnEsc) { - onClose(event, 'pressedEscape') - } - } - } - } - }, [ - h(Box, { - props: { - as: 'section', - outline: 0, - maxWidth: size, - w: '100%', - pos: 'relative', - d: 'flex', - flexDir: 'column', - zIndex: this.zIndex, - fontFamily: 'body', - ...this.boxStyleProps, - ...this.contentStyle, - ...forwardProps(this.$props) - }, - attrs: { - role: 'dialog', - 'aria-modal': 'true', - tabIndex: -1, - id: contentId, - ...(addAriaDescribedby && { 'aria-describedby': bodyId }), - ...(addAriaLabelledby && { 'aria-labelledby': headerId }), - ...this.$attrs - }, - nativeOn: { - click: wrapEvent((e) => this.$emit('click', e), event => event.stopPropagation()) - } - }, this.$slots.default) - ]) - } -} - -const ModalHeader = { - name: 'ModalHeader', - inject: ['$ModalContext'], - props: baseProps, - computed: { - context () { - return this.$ModalContext() - } - }, - render (h) { - const { headerId } = this.context - - return h(Box, { - props: { - as: 'header', - px: 6, - py: 4, - position: 'relative', - fontSize: 'xl', - fontWeight: 'semibold', - ...forwardProps(this.$props) - }, - attrs: { - id: headerId - } - }, this.$slots.default) - } -} - -const ModalFooter = { - name: 'ModalFooter', - props: baseProps, - render (h) { - return h(Box, { - props: { - as: 'footer', - display: 'flex', - px: 6, - py: 4, - justifyContent: 'flex-end', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const ModalBody = { - name: 'ModalBody', - props: baseProps, - inject: ['$ModalContext'], - computed: { - context () { - return this.$ModalContext() - } - }, - render (h) { - const { bodyId, scrollBehavior } = this.context - - let style = {} - if (scrollBehavior === 'inside') { - style = { overflowY: 'auto' } - } - - return h(Box, { - props: { - px: 6, - py: 2, - flex: 1, - ...style, - ...forwardProps(this.$props) - }, - attrs: { - id: bodyId - } - }, this.$slots.default) - } -} - -const ModalCloseButton = { - name: 'ModalCloseButton', - props: styleProps, - inject: ['$ModalContext'], - computed: { - context () { - return this.$ModalContext() - } - }, - render (h) { - const { onClose } = this.context - return h(CloseButton, { - props: { - position: 'absolute', - top: '8px', - right: '12px', - ...forwardProps(this.$props) - }, - attrs: { - 'x-close-button': '' - }, - on: { - click: (e) => { - wrapEvent(onClose, event => this.$emit('click', event))(e) - } - } - }) - } -} - -export { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - ModalCloseButton -} +export * from './Modal' diff --git a/packages/chakra-ui-core/src/NoSsr/NoSsr.js b/packages/chakra-ui-core/src/NoSsr/NoSsr.js new file mode 100644 index 00000000..a93dffb4 --- /dev/null +++ b/packages/chakra-ui-core/src/NoSsr/NoSsr.js @@ -0,0 +1,42 @@ +/** + * This component is a modification of the fine work of @egoist + * @see https://github.com/egoist/vue-client-only/blob/master/src/index.js + */ +const NoSsr = { + name: 'NoSsr', + functional: true, + props: { + placeholder: String, + placeholderTag: { + type: String, + default: 'div' + } + }, + render (h, { parent, slots, props }) { + const { default: defaultSlot = [], placeholder: placeholderSlot } = slots() + + if (parent._isMounted) { + return defaultSlot + } + + parent.$once('hook:mounted', () => { + parent.$forceUpdate() + }) + + if (props.placeholderTag && (props.placeholder || placeholderSlot)) { + return h( + props.placeholderTag, + { + class: ['client-only-placeholder'] + }, + props.placeholder || placeholderSlot + ) + } + + // Return a placeholder element for each child in the default slot + // Or if no children return a single placeholder + return defaultSlot.length > 0 ? defaultSlot.map(() => h(false)) : h(false) + } +} + +export default NoSsr diff --git a/packages/chakra-ui-core/src/NoSsr/index.js b/packages/chakra-ui-core/src/NoSsr/index.js index a93dffb4..c1f6b076 100644 --- a/packages/chakra-ui-core/src/NoSsr/index.js +++ b/packages/chakra-ui-core/src/NoSsr/index.js @@ -1,42 +1,2 @@ -/** - * This component is a modification of the fine work of @egoist - * @see https://github.com/egoist/vue-client-only/blob/master/src/index.js - */ -const NoSsr = { - name: 'NoSsr', - functional: true, - props: { - placeholder: String, - placeholderTag: { - type: String, - default: 'div' - } - }, - render (h, { parent, slots, props }) { - const { default: defaultSlot = [], placeholder: placeholderSlot } = slots() - - if (parent._isMounted) { - return defaultSlot - } - - parent.$once('hook:mounted', () => { - parent.$forceUpdate() - }) - - if (props.placeholderTag && (props.placeholder || placeholderSlot)) { - return h( - props.placeholderTag, - { - class: ['client-only-placeholder'] - }, - props.placeholder || placeholderSlot - ) - } - - // Return a placeholder element for each child in the default slot - // Or if no children return a single placeholder - return defaultSlot.length > 0 ? defaultSlot.map(() => h(false)) : h(false) - } -} - +import NoSsr from './NoSsr' export default NoSsr diff --git a/packages/chakra-ui-core/src/NumberInput/NumberInput.js b/packages/chakra-ui-core/src/NumberInput/NumberInput.js new file mode 100644 index 00000000..46f7f5a7 --- /dev/null +++ b/packages/chakra-ui-core/src/NumberInput/NumberInput.js @@ -0,0 +1,625 @@ +import { baseProps } from '../config' +import Flex from '../Flex' +import styleProps from '../config/props' +import Input from '../Input' +import PseudoBox from '../PseudoBox' +import Icon from '../Icon' +import numberInputStyles from './numberinput.styles' +import { isDef, useId, getElement, canUseDOM, wrapEvent } from '../utils' +import { calculatePrecision, roundToPrecision, preventNonNumberKey } from './utils' +import { inputProps } from '../Input/input.props' + +/** + * NumberInput component + */ +const NumberInput = { + name: 'NumberInput', + props: { + ...baseProps, + value: Number, + defaultValue: Number, + focusInputOnChange: { + type: Boolean, + default: true + }, + clampValueOnBlur: { + type: Boolean, + default: true + }, + keepWithinRange: { + type: Boolean, + default: true + }, + min: { + type: Number, + default: -Infinity + }, + max: { + type: Number, + default: Infinity + }, + step: { + type: Number, + default: 1 + }, + precision: Number, + getAriaValueText: Function, + isReadOnly: Boolean, + isInvalid: Boolean, + isDisabled: Boolean, + isFullWidth: Boolean, + size: { + type: String, + default: 'md' + }, + inputId: { + type: String, + default: `number-input-${useId()}` + } + }, + provide () { + return { + $NumberInputContext: () => this.NumberInputContext + } + }, + data () { + return { + innerValue: this.defaultValue || null, + isFocused: false, + prevNexValue: null, + inputNode: undefined, + incrementPressed: false, + decrementPressed: false, + incrementEvents: {}, + decrementEvents: {}, + clickEvent: canUseDOM && !!document.documentElement.ontouchstart + ? 'touchstart' + : 'mousedown', + incrementStepperProps: undefined, + decrementStepperProps: undefined, + incrementTimerId: null, + decrementTimerId: null + } + }, + computed: { + NumberInputContext () { + return { + size: this.size, + value: this._value, + isReadOnly: this.isReadOnly, + isInvalid: this.isInvalid, + isDisabled: this.isDisabled, + isFocused: this.isFocused, + incrementStepper: this.incrementStepperProps, + decrementStepper: this.decrementStepperProps, + incrementButton: { + nativeOn: { + click: () => this.handleIncrement() + }, + attrs: { + 'aria-label': 'add', + ...(this.keepWithinRange & { + disabled: this.value === this.max, + 'aria-disabled': this.value === this.max + }) + } + }, + decrementButton: { + nativeOn: { + click: () => this.handleDecrement() + }, + attrs: { + 'aria-label': 'subtract', + ...(this.keepWithinRange & { + disabled: this.value === this.min, + 'aria-disabled': this.value === this.min + }) + } + }, + input: { + value: this._value, + onChange: this.handleChange, + onKeydown: this.handleKeydown, + onFocus: () => { + this.isFocused = true + }, + onBlur: () => { + this.isFocused = false + if (this.clampValueOnBlur) { + this.validateAndClamp() + } + }, + role: 'spinbutton', + type: 'text', + 'aria-valuemin': this.min, + 'aria-valuemax': this.max, + 'aria-disabled': this.isDisabled, + 'aria-valuenow': this.value, + 'aria-invalid': this.isInvalid || this.isOutOfRange, + ...(this.getAriaValueText && { 'aria-valuetext': this.ariaValueText }), + readOnly: this.isReadOnly, + disabled: this.isDisabled, + autoComplete: 'off' + }, + hiddenLabel: { + 'aria-live': 'polite', + text: this.getAriaValueText ? this.ariaValueText : this._value, + style: { + position: 'absolute', + clip: 'rect(0px, 0px, 0px, 0px)', + height: 1, + width: 1, + margin: -1, + whiteSpace: 'nowrap', + border: 0, + overflow: 'hidden', + padding: 0 + } + }, + inputId: this._inputId + } + }, + isControlled () { + return isDef(this.value) + }, + _value: { + get () { + return this.isControlled + ? roundToPrecision(this.value, this._precision) + : this.innerValue + ? roundToPrecision(this.innerValue, this._precision) + : this.innerValue + }, + set (val) { + if (!this.defaultValue) { + let nextValue = this.defaultValue + if (this.keepWithinRange) { + nextValue = Math.max(Math.min(nextValue, this.max), this.min) + } + nextValue = roundToPrecision(nextValue, this._precision) + this.innerValue = nextValue + } + this.innerValue = val + } + }, + defaultPrecision () { + return Math.max(calculatePrecision(this.step), 0) + }, + _precision () { + return this.precision || this.defaultPrecision + }, + isInteractive () { + return !(this.isReadOnly || this.isDisabled) + }, + isOutOfRange () { + return this._value > this.max || this.value < this.min + }, + ariaValueText () { + return this.getAriaValueText ? this.getAriaValueText(this._value) : null + }, + _inputId () { + return `number-input-${this.inputId || useId()}` + } + }, + mounted () { + this.$nextTick(() => { + this.inputNode = getElement(`#${this._inputId}`, this.$el) + }) + + // ================================= INCREMENT WATCHER + this.$watch(vm => [vm.incrementPressed, vm._value], () => { + if (this.incrementTimerId) clearTimeout(this.incrementTimerId) + if (this.incrementPressed) { + this.incrementTimerId = setTimeout(this.handleIncrement, 200) + } else { + clearTimeout(this.incrementTimerId) + } + }) + + const startIncrement = () => { + this.handleIncrement() + this.incrementPressed = true + } + const stopIncrement = () => { + this.incrementPressed = false + } + + this.incrementStepperProps = { + [this.clickEvent]: startIncrement, + mouseup: stopIncrement, + mouseleave: stopIncrement, + touchend: stopIncrement + } + + // ================================= DECREMENT WATCHER + this.$watch(vm => [vm.decrementPressed, vm._value], () => { + if (this.decrementTimerId) clearTimeout(this.decrementTimerId) + if (this.decrementPressed) { + this.decrementTimerId = setTimeout(this.handleDecrement, 200) + } else { + clearTimeout(this.decrementTimerId) + } + }) + + const startDecrement = () => { + this.handleDecrement() + this.decrementPressed = true + } + const stopDecrement = () => { + this.decrementPressed = false + } + + this.decrementStepperProps = { + [this.clickEvent]: startDecrement, + mouseup: stopDecrement, + mouseleave: stopDecrement, + touchend: stopDecrement + } + }, + methods: { + + /** + * Validates and clamps input values + */ + validateAndClamp () { + const maxExists = isDef(this.max) + const minExists = isDef(this.min) + + if (maxExists && this._value > this.max) { + this.updateValue(this.max) + } + + if (minExists && this._value < this.min) { + this.updateValue(this.min) + } + }, + + /** + * Get increment factor + */ + getIncrementFactor (event) { + let ratio = 1 + if (event.metaKey || event.ctrlKey) { + ratio = 0.1 + } + if (event.shiftKey) { + ratio = 10 + } + return ratio + }, + + /** + * Determines whether a value should be converted to number + * @param {String} value + */ + shouldConvertToNumber (value) { + const _value = typeof value !== 'string' ? String(value) : value + const hasDot = _value.indexOf('.') > -1 + const hasTrailingZero = _value.substr(_value.length - 1) === '0' + const hasTrailingDot = _value.substr(_value.length - 1) === '.' + if (hasDot && hasTrailingZero) return false + if (hasDot && hasTrailingDot) return false + return true + }, + + /** + * Updates the current input value + * @param {Number|String} nextValue value + */ + updateValue (nextValue) { + if (this.prevNextValue === nextValue) return + const shouldConvert = this.shouldConvertToNumber(nextValue) + const converted = shouldConvert ? +nextValue : nextValue + if (!this.isControlled) { + this._value = converted + } + + this.$emit('change', converted) + + this.prevNextValue = nextValue + }, + + /** + * Handles value increment + * @param {Number} step Value to be incremented + */ + handleIncrement (step = this.step) { + if (!this.isInteractive) return + let nextValue = Number(this._value) + Number(step) + + if (this.keepWithinRange) { + nextValue = Math.min(nextValue, this.max) + } + + nextValue = roundToPrecision(nextValue, this._precision) + this.updateValue(nextValue) + this.$emit('increment', nextValue) + this.focusInput() + }, + + /** + * Handles value decrement + * @param {Number} step Value to be decremented + */ + handleDecrement (step = this.step) { + if (!this.isInteractive) return + let nextValue = Number(this._value) - Number(step) + + if (this.keepWithinRange) { + nextValue = Math.max(nextValue, this.min) + } + + nextValue = roundToPrecision(nextValue, this._precision) + this.updateValue(nextValue) + this.$emit('decrement', nextValue) + this.focusInput() + }, + + /** + * Focus NumberInput element + */ + focusInput () { + const _this = this + requestAnimationFrame(() => { + _this.inputNode && _this.inputNode.focus() + }) + }, + + /** + * Handles "blur" event + * @param {Event} event Event object + */ + handleBlur (event) { + this.$emit('blur', event) + }, + + /** + * Handles "focus" event + * @param {Event} event Event object + */ + handleFocus (event) { + this.$emit('focus', event) + }, + + /** + * Handles "keydown" event + * @param {Event} event Event object + */ + handleKeydown (event) { + this.$emit('keydown', event) + preventNonNumberKey(event) + if (!this.isInteractive) return + + if (event.key === 'ArrowUp') { + event.preventDefault() + const ratio = this.getIncrementFactor(event) + this.handleIncrement(ratio * this.step) + } + + if (event.key === 'ArrowDown') { + event.preventDefault() + const ratio = this.getIncrementFactor(event) + this.handleDecrement(ratio * this.step) + } + + if (event.key === 'Home') { + event.preventDefault() + if (isDef(this.min)) { + this.updateValue(this.max) + } + } + + if (event.key === 'End') { + event.preventDefault() + if (isDef(this.max)) { + this.updateValue(this.min) + } + } + }, + + /** + * + * @param {Event} event Event object + * @param {Any} event Value + */ + handleChange (event, value) { + this.updateValue(event.target.value) + this.$emit('change', event, value) + } + }, + render (h) { + const { size, ...styles } = this.$props + return h(Flex, { + props: { + ...styles, + align: 'stretch', + w: this.isFullWidth ? 'full' : null, + pos: 'relative' + } + }, this.$slots.default) + } +} + +/** + * NumberInputField component + */ +const NumberInputField = { + name: 'NumberInputField', + inject: ['$NumberInputContext'], + computed: { + context () { + return this.$NumberInputContext() + } + }, + props: { + ...styleProps, + ...inputProps + }, + render (h) { + const { size, inputId, input: { + value, + onBlur: _onBlur, + onFocus: _onFocus, + onChange: _onChange, + onKeydown: _onKeydown, + disabled: isDisabled, + readOnly: isReadOnly, + ...otherInputAttrs + } + } = this.context + + return h(Input, { + props: { + ...this.$props, + isReadOnly, + isDisabled, + size, + value + }, + attrs: { + id: inputId, + ...otherInputAttrs + }, + on: { + change: wrapEvent((e) => this.$emit('change', e), _onChange) + }, + nativeOn: { + input: wrapEvent((e) => this.$emit('change', e), _onChange), + blur: wrapEvent((e) => this.$emit('blur', e), _onBlur), + focus: wrapEvent((e) => this.$emit('focus', e), _onFocus), + keydown: wrapEvent((e) => this.$emit('keydown', e), _onKeydown) + } + }) + } +} + +/** + * NumberInputStepper component + */ +const NumberInputStepper = { + name: 'NumberInputStepper', + props: baseProps, + render (h) { + return h(Flex, { + props: { + ...this.$props, + direction: 'column', + width: '24px', + margin: '1px', + position: 'absolute', + right: '0px', + height: 'calc(100% - 2px)', + zIndex: 1 + } + }, this.$slots.default) + } +} + +/** + * StepperButton component + */ +const StepperButton = { + name: 'StepperButton', + inject: ['$NumberInputContext', '$colorMode'], + props: styleProps, + computed: { + context () { + return this.$NumberInputContext() + }, + colorMode () { + return this.$colorMode() + } + }, + render (h) { + const { isDisabled, size } = this.context + return h(PseudoBox, { + props: { + ...this.$props, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flex: 1, + transition: 'all 0.3s', + userSelect: 'none', + pointerEvents: isDisabled ? 'none' : undefined, + lineHeight: 'normal', + ...numberInputStyles({ + colorMode: this.colorMode, + size + }) + } + }, this.$slots.default) + } +} + +const NumberIncrementStepper = { + name: 'NumberIncrementStepper', + inject: ['$NumberInputContext'], + computed: { + context () { + return this.$NumberInputContext() + } + }, + props: styleProps, + render (h) { + const children = this.$slots.default || + [h(Icon, { + props: { + name: 'triangle-up', + height: '0.6em', + width: '0.6em' + } + })] + + const { size, incrementStepper } = this.context + const iconSize = size === 'sm' ? '11px' : '15px' + + return h(StepperButton, { + props: { + ...this.$props, + fontSize: iconSize + }, + nativeOn: incrementStepper + }, children) + } +} + +const NumberDecrementStepper = { + name: 'NumberDecrementStepper', + inject: ['$NumberInputContext'], + computed: { + context () { + return this.$NumberInputContext() + } + }, + props: styleProps, + render (h) { + const children = this.$slots.default || + [h(Icon, { + props: { + name: 'triangle-down', + height: '0.6em', + width: '0.6em' + } + })] + + const { size, decrementStepper } = this.context + const iconSize = size === 'sm' ? '11px' : '15px' + + return h(StepperButton, { + props: { + ...this.$props, + fontSize: iconSize + }, + nativeOn: decrementStepper + }, children) + } +} + +export { + NumberInput, + NumberInputField, + NumberInputStepper, + NumberIncrementStepper, + NumberDecrementStepper +} diff --git a/packages/chakra-ui-core/src/NumberInput/index.js b/packages/chakra-ui-core/src/NumberInput/index.js index 46f7f5a7..89e782f1 100644 --- a/packages/chakra-ui-core/src/NumberInput/index.js +++ b/packages/chakra-ui-core/src/NumberInput/index.js @@ -1,625 +1 @@ -import { baseProps } from '../config' -import Flex from '../Flex' -import styleProps from '../config/props' -import Input from '../Input' -import PseudoBox from '../PseudoBox' -import Icon from '../Icon' -import numberInputStyles from './numberinput.styles' -import { isDef, useId, getElement, canUseDOM, wrapEvent } from '../utils' -import { calculatePrecision, roundToPrecision, preventNonNumberKey } from './utils' -import { inputProps } from '../Input/input.props' - -/** - * NumberInput component - */ -const NumberInput = { - name: 'NumberInput', - props: { - ...baseProps, - value: Number, - defaultValue: Number, - focusInputOnChange: { - type: Boolean, - default: true - }, - clampValueOnBlur: { - type: Boolean, - default: true - }, - keepWithinRange: { - type: Boolean, - default: true - }, - min: { - type: Number, - default: -Infinity - }, - max: { - type: Number, - default: Infinity - }, - step: { - type: Number, - default: 1 - }, - precision: Number, - getAriaValueText: Function, - isReadOnly: Boolean, - isInvalid: Boolean, - isDisabled: Boolean, - isFullWidth: Boolean, - size: { - type: String, - default: 'md' - }, - inputId: { - type: String, - default: `number-input-${useId()}` - } - }, - provide () { - return { - $NumberInputContext: () => this.NumberInputContext - } - }, - data () { - return { - innerValue: this.defaultValue || null, - isFocused: false, - prevNexValue: null, - inputNode: undefined, - incrementPressed: false, - decrementPressed: false, - incrementEvents: {}, - decrementEvents: {}, - clickEvent: canUseDOM && !!document.documentElement.ontouchstart - ? 'touchstart' - : 'mousedown', - incrementStepperProps: undefined, - decrementStepperProps: undefined, - incrementTimerId: null, - decrementTimerId: null - } - }, - computed: { - NumberInputContext () { - return { - size: this.size, - value: this._value, - isReadOnly: this.isReadOnly, - isInvalid: this.isInvalid, - isDisabled: this.isDisabled, - isFocused: this.isFocused, - incrementStepper: this.incrementStepperProps, - decrementStepper: this.decrementStepperProps, - incrementButton: { - nativeOn: { - click: () => this.handleIncrement() - }, - attrs: { - 'aria-label': 'add', - ...(this.keepWithinRange & { - disabled: this.value === this.max, - 'aria-disabled': this.value === this.max - }) - } - }, - decrementButton: { - nativeOn: { - click: () => this.handleDecrement() - }, - attrs: { - 'aria-label': 'subtract', - ...(this.keepWithinRange & { - disabled: this.value === this.min, - 'aria-disabled': this.value === this.min - }) - } - }, - input: { - value: this._value, - onChange: this.handleChange, - onKeydown: this.handleKeydown, - onFocus: () => { - this.isFocused = true - }, - onBlur: () => { - this.isFocused = false - if (this.clampValueOnBlur) { - this.validateAndClamp() - } - }, - role: 'spinbutton', - type: 'text', - 'aria-valuemin': this.min, - 'aria-valuemax': this.max, - 'aria-disabled': this.isDisabled, - 'aria-valuenow': this.value, - 'aria-invalid': this.isInvalid || this.isOutOfRange, - ...(this.getAriaValueText && { 'aria-valuetext': this.ariaValueText }), - readOnly: this.isReadOnly, - disabled: this.isDisabled, - autoComplete: 'off' - }, - hiddenLabel: { - 'aria-live': 'polite', - text: this.getAriaValueText ? this.ariaValueText : this._value, - style: { - position: 'absolute', - clip: 'rect(0px, 0px, 0px, 0px)', - height: 1, - width: 1, - margin: -1, - whiteSpace: 'nowrap', - border: 0, - overflow: 'hidden', - padding: 0 - } - }, - inputId: this._inputId - } - }, - isControlled () { - return isDef(this.value) - }, - _value: { - get () { - return this.isControlled - ? roundToPrecision(this.value, this._precision) - : this.innerValue - ? roundToPrecision(this.innerValue, this._precision) - : this.innerValue - }, - set (val) { - if (!this.defaultValue) { - let nextValue = this.defaultValue - if (this.keepWithinRange) { - nextValue = Math.max(Math.min(nextValue, this.max), this.min) - } - nextValue = roundToPrecision(nextValue, this._precision) - this.innerValue = nextValue - } - this.innerValue = val - } - }, - defaultPrecision () { - return Math.max(calculatePrecision(this.step), 0) - }, - _precision () { - return this.precision || this.defaultPrecision - }, - isInteractive () { - return !(this.isReadOnly || this.isDisabled) - }, - isOutOfRange () { - return this._value > this.max || this.value < this.min - }, - ariaValueText () { - return this.getAriaValueText ? this.getAriaValueText(this._value) : null - }, - _inputId () { - return `number-input-${this.inputId || useId()}` - } - }, - mounted () { - this.$nextTick(() => { - this.inputNode = getElement(`#${this._inputId}`, this.$el) - }) - - // ================================= INCREMENT WATCHER - this.$watch(vm => [vm.incrementPressed, vm._value], () => { - if (this.incrementTimerId) clearTimeout(this.incrementTimerId) - if (this.incrementPressed) { - this.incrementTimerId = setTimeout(this.handleIncrement, 200) - } else { - clearTimeout(this.incrementTimerId) - } - }) - - const startIncrement = () => { - this.handleIncrement() - this.incrementPressed = true - } - const stopIncrement = () => { - this.incrementPressed = false - } - - this.incrementStepperProps = { - [this.clickEvent]: startIncrement, - mouseup: stopIncrement, - mouseleave: stopIncrement, - touchend: stopIncrement - } - - // ================================= DECREMENT WATCHER - this.$watch(vm => [vm.decrementPressed, vm._value], () => { - if (this.decrementTimerId) clearTimeout(this.decrementTimerId) - if (this.decrementPressed) { - this.decrementTimerId = setTimeout(this.handleDecrement, 200) - } else { - clearTimeout(this.decrementTimerId) - } - }) - - const startDecrement = () => { - this.handleDecrement() - this.decrementPressed = true - } - const stopDecrement = () => { - this.decrementPressed = false - } - - this.decrementStepperProps = { - [this.clickEvent]: startDecrement, - mouseup: stopDecrement, - mouseleave: stopDecrement, - touchend: stopDecrement - } - }, - methods: { - - /** - * Validates and clamps input values - */ - validateAndClamp () { - const maxExists = isDef(this.max) - const minExists = isDef(this.min) - - if (maxExists && this._value > this.max) { - this.updateValue(this.max) - } - - if (minExists && this._value < this.min) { - this.updateValue(this.min) - } - }, - - /** - * Get increment factor - */ - getIncrementFactor (event) { - let ratio = 1 - if (event.metaKey || event.ctrlKey) { - ratio = 0.1 - } - if (event.shiftKey) { - ratio = 10 - } - return ratio - }, - - /** - * Determines whether a value should be converted to number - * @param {String} value - */ - shouldConvertToNumber (value) { - const _value = typeof value !== 'string' ? String(value) : value - const hasDot = _value.indexOf('.') > -1 - const hasTrailingZero = _value.substr(_value.length - 1) === '0' - const hasTrailingDot = _value.substr(_value.length - 1) === '.' - if (hasDot && hasTrailingZero) return false - if (hasDot && hasTrailingDot) return false - return true - }, - - /** - * Updates the current input value - * @param {Number|String} nextValue value - */ - updateValue (nextValue) { - if (this.prevNextValue === nextValue) return - const shouldConvert = this.shouldConvertToNumber(nextValue) - const converted = shouldConvert ? +nextValue : nextValue - if (!this.isControlled) { - this._value = converted - } - - this.$emit('change', converted) - - this.prevNextValue = nextValue - }, - - /** - * Handles value increment - * @param {Number} step Value to be incremented - */ - handleIncrement (step = this.step) { - if (!this.isInteractive) return - let nextValue = Number(this._value) + Number(step) - - if (this.keepWithinRange) { - nextValue = Math.min(nextValue, this.max) - } - - nextValue = roundToPrecision(nextValue, this._precision) - this.updateValue(nextValue) - this.$emit('increment', nextValue) - this.focusInput() - }, - - /** - * Handles value decrement - * @param {Number} step Value to be decremented - */ - handleDecrement (step = this.step) { - if (!this.isInteractive) return - let nextValue = Number(this._value) - Number(step) - - if (this.keepWithinRange) { - nextValue = Math.max(nextValue, this.min) - } - - nextValue = roundToPrecision(nextValue, this._precision) - this.updateValue(nextValue) - this.$emit('decrement', nextValue) - this.focusInput() - }, - - /** - * Focus NumberInput element - */ - focusInput () { - const _this = this - requestAnimationFrame(() => { - _this.inputNode && _this.inputNode.focus() - }) - }, - - /** - * Handles "blur" event - * @param {Event} event Event object - */ - handleBlur (event) { - this.$emit('blur', event) - }, - - /** - * Handles "focus" event - * @param {Event} event Event object - */ - handleFocus (event) { - this.$emit('focus', event) - }, - - /** - * Handles "keydown" event - * @param {Event} event Event object - */ - handleKeydown (event) { - this.$emit('keydown', event) - preventNonNumberKey(event) - if (!this.isInteractive) return - - if (event.key === 'ArrowUp') { - event.preventDefault() - const ratio = this.getIncrementFactor(event) - this.handleIncrement(ratio * this.step) - } - - if (event.key === 'ArrowDown') { - event.preventDefault() - const ratio = this.getIncrementFactor(event) - this.handleDecrement(ratio * this.step) - } - - if (event.key === 'Home') { - event.preventDefault() - if (isDef(this.min)) { - this.updateValue(this.max) - } - } - - if (event.key === 'End') { - event.preventDefault() - if (isDef(this.max)) { - this.updateValue(this.min) - } - } - }, - - /** - * - * @param {Event} event Event object - * @param {Any} event Value - */ - handleChange (event, value) { - this.updateValue(event.target.value) - this.$emit('change', event, value) - } - }, - render (h) { - const { size, ...styles } = this.$props - return h(Flex, { - props: { - ...styles, - align: 'stretch', - w: this.isFullWidth ? 'full' : null, - pos: 'relative' - } - }, this.$slots.default) - } -} - -/** - * NumberInputField component - */ -const NumberInputField = { - name: 'NumberInputField', - inject: ['$NumberInputContext'], - computed: { - context () { - return this.$NumberInputContext() - } - }, - props: { - ...styleProps, - ...inputProps - }, - render (h) { - const { size, inputId, input: { - value, - onBlur: _onBlur, - onFocus: _onFocus, - onChange: _onChange, - onKeydown: _onKeydown, - disabled: isDisabled, - readOnly: isReadOnly, - ...otherInputAttrs - } - } = this.context - - return h(Input, { - props: { - ...this.$props, - isReadOnly, - isDisabled, - size, - value - }, - attrs: { - id: inputId, - ...otherInputAttrs - }, - on: { - change: wrapEvent((e) => this.$emit('change', e), _onChange) - }, - nativeOn: { - input: wrapEvent((e) => this.$emit('change', e), _onChange), - blur: wrapEvent((e) => this.$emit('blur', e), _onBlur), - focus: wrapEvent((e) => this.$emit('focus', e), _onFocus), - keydown: wrapEvent((e) => this.$emit('keydown', e), _onKeydown) - } - }) - } -} - -/** - * NumberInputStepper component - */ -const NumberInputStepper = { - name: 'NumberInputStepper', - props: baseProps, - render (h) { - return h(Flex, { - props: { - ...this.$props, - direction: 'column', - width: '24px', - margin: '1px', - position: 'absolute', - right: '0px', - height: 'calc(100% - 2px)', - zIndex: 1 - } - }, this.$slots.default) - } -} - -/** - * StepperButton component - */ -const StepperButton = { - name: 'StepperButton', - inject: ['$NumberInputContext', '$colorMode'], - props: styleProps, - computed: { - context () { - return this.$NumberInputContext() - }, - colorMode () { - return this.$colorMode() - } - }, - render (h) { - const { isDisabled, size } = this.context - return h(PseudoBox, { - props: { - ...this.$props, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - flex: 1, - transition: 'all 0.3s', - userSelect: 'none', - pointerEvents: isDisabled ? 'none' : undefined, - lineHeight: 'normal', - ...numberInputStyles({ - colorMode: this.colorMode, - size - }) - } - }, this.$slots.default) - } -} - -const NumberIncrementStepper = { - name: 'NumberIncrementStepper', - inject: ['$NumberInputContext'], - computed: { - context () { - return this.$NumberInputContext() - } - }, - props: styleProps, - render (h) { - const children = this.$slots.default || - [h(Icon, { - props: { - name: 'triangle-up', - height: '0.6em', - width: '0.6em' - } - })] - - const { size, incrementStepper } = this.context - const iconSize = size === 'sm' ? '11px' : '15px' - - return h(StepperButton, { - props: { - ...this.$props, - fontSize: iconSize - }, - nativeOn: incrementStepper - }, children) - } -} - -const NumberDecrementStepper = { - name: 'NumberDecrementStepper', - inject: ['$NumberInputContext'], - computed: { - context () { - return this.$NumberInputContext() - } - }, - props: styleProps, - render (h) { - const children = this.$slots.default || - [h(Icon, { - props: { - name: 'triangle-down', - height: '0.6em', - width: '0.6em' - } - })] - - const { size, decrementStepper } = this.context - const iconSize = size === 'sm' ? '11px' : '15px' - - return h(StepperButton, { - props: { - ...this.$props, - fontSize: iconSize - }, - nativeOn: decrementStepper - }, children) - } -} - -export { - NumberInput, - NumberInputField, - NumberInputStepper, - NumberIncrementStepper, - NumberDecrementStepper -} +export * from './NumberInput' diff --git a/packages/chakra-ui-core/src/Popover/Popover.js b/packages/chakra-ui-core/src/Popover/Popover.js new file mode 100644 index 00000000..a37961e3 --- /dev/null +++ b/packages/chakra-ui-core/src/Popover/Popover.js @@ -0,0 +1,586 @@ +import Fragment from '../Fragment' +import { Popper, PopperArrow } from '../Popper' +import { useId, cloneVNode, getElement, isVueComponent, forwardProps } from '../utils' +import styleProps, { baseProps } from '../config/props' +import Box from '../Box' +import CloseButton from '../CloseButton' + +const Popover = { + name: 'Popover', + provide () { + return { + $PopoverContext: () => this.PopoverContext + } + }, + props: { + id: { + type: String, + default: `popover-id-${useId()}` + }, + defaultIsOpen: Boolean, + isOpen: Boolean, + returnFocusOnClose: { + type: Boolean, + default: true + }, + initialFocusRef: [Object, String], + trigger: { + type: String, + default: 'click' + }, + closeOnBlur: { + type: Boolean, + default: true + }, + closeOnEscape: { + type: Boolean, + default: true + }, + usePortal: Boolean, + placement: { + type: String, + default: 'auto' + } + }, + computed: { + PopoverContext () { + return { + set: this.set, + isOpen: this._isOpen, + closePopover: this.closePopover, + openPopover: this.openPopover, + toggleOpen: this.toggleOpen, + triggerNode: this.triggerNode, + contentNode: this.contentNode, + setTriggerNode: this.setTriggerNode, + popoverId: this.id, + trigger: this.trigger, + isHovering: this.isHovering, + handleBlur: this.handleBlur, + closeOnEscape: this.closeOnEscape, + headerId: this.headerId, + bodyId: this.bodyId, + usePortal: this.usePortal, + placement: this.placement + } + }, + isControlled () { + return this.isOpen !== false + }, + _isOpen: { + get () { + return this.isControlled ? this.isOpen : this.isOpenValue + }, + set (value) { + this.isOpenValue = value + } + }, + _initialFocusRef () { + return this.getNode(this.initialFocusRef) + }, + headerId () { + return `${this.id}-header` + }, + bodyId () { + return `${this.id}-body` + } + }, + data () { + return { + isOpenValue: this.defaultIsOpen || false, + triggerNode: undefined, + contentNode: undefined, + prevIsOpen: false, + isHovering: false + } + }, + mounted () { + /** + * The purpose of this watcher is to keep record of the previous + * isOpen value. + */ + this.$watch('_isOpen', (_newVal, oldVal) => { + this.prevIsOpen = oldVal + }, { + immediate: true + }) + + this.$watch(vm => [ + vm._isOpen, + vm._initialFocusRef, + vm.trigger, + vm.contentNode, + vm.triggerNode, + vm.prevIsOpen, + vm.returnFocusOnClose + ], () => { + if (this._isOpen && this.trigger === 'click') { + /** + * Caveat here: + * Until Vue 3 is reease, using it's $refs as props may not always return a value + * in the props unless the consumer component updates it's context. This is because + * Vue asynchronously updtaes the DOM and is also not reactive. + * + * Where this doesnt' work, we fallback to using an element selector to query + * the element from the DOM. And use it as the initial focus ref. + * + * Work-around could be to use plain old JS selectors + */ + setTimeout(() => { + if (this._initialFocusRef) { + this._initialFocusRef.focus() + } else { + if (this.contentNode) { + this.contentNode.focus() + } + } + }) + } + + if (!this._isOpen && this.prevIsOpen && this.trigger === 'click' && this.returnFocusOnClose) { + if (this.triggerNode) { + this.triggerNode.focus() + } + } + }) + }, + methods: { + /** + * Closes popover + */ + closePopover () { + if (!this.isControlled) { + this._isOpen = false + } + this.$emit('close') + }, + /** + * Opens popover + */ + openPopover () { + if (!this.isControlled) { + this._isOpen = true + } + this.$emit('open') + }, + /** + * Toggles disclosure state of popover + */ + toggleOpen () { + if (!this.isControlled) { + this._isOpen = !this._isOpen + } + + if (this._isOpen !== true) { + this.$emit('open') + } else { + this.$emit('close') + } + }, + /** + * Handles blur event + * @param {Event} e `blur` event object + */ + handleBlur (event) { + if ( + this._isOpen && + this.closeOnBlur && + this.contentNode && + this.triggerNode && + !this.contentNode.contains(event.relatedTarget) && + !this.triggerNode.contains(event.relatedTarget) + ) { + this.closePopover() + } + }, + /** + * Returns the HTML element of a Vue component or native element + * @param {Vue.Component|HTMLElement|String} element HTMLElement or Vue Component + */ + getNode (element) { + if (typeof element === 'object') { + const isVue = isVueComponent(element) + return isVue ? element.$el : element + } else if (typeof element === 'string') { + return getElement(element) + } + return null + }, + /** + * Sets the value of any component instance property. + * This function is to be passed down to context so that consumers + * can mutate context values with out doing it directly. + * Serves as a temporary fix until Vue 3 comes out + * @param {String} prop Component instance property + * @param {Any} value Property value + */ + set (prop, value) { + this[prop] = value + return this[prop] + } + }, + render (h) { + return h(Fragment, [ + this.$scopedSlots.default({ + isOpen: this._isOpen, + onClose: this.closePopover + }) + ]) + } +} + +const PopoverTrigger = { + name: 'PopoverTrigger', + inject: ['$PopoverContext'], + computed: { + triggerId () { + return `popover-trigger-${useId()}` + }, + context () { + return this.$PopoverContext() + }, + headerId () { + return this.context.headerId + }, + bodyId () { + return this.context.bodyId + }, + eventHandlers () { + const { trigger } = this.context + + if (trigger === 'click') { + return { + click: (e) => { + this.$emit('click', e) + this.context.toggleOpen() + } + } + } + + if (trigger === 'hover') { + return { + focus: (e) => { + this.$emit('focus', e) + this.context.openPopover() + }, + keydown: (e) => { + this.$emit('keydown', e) + if (e.key === 'Escape') { + setTimeout(this.context.closePopover(), 300) + } + }, + blur: (e) => { + this.$emit('blur', e) + this.context.closePopover() + }, + mouseenter: (e) => { + this.$emit('mouseenter', e) + this.context.set('isHovering', true) + setTimeout(this.context.openPopover(), 300) + }, + mouseleave: (e) => { + this.$emit('mouseleave', e) + this.context.set('isHovering', false) + setTimeout(() => { + if (this.context.isHovering === false) { + this.context.closePopover() + } + }, 300) + } + } + } + } + }, + mounted () { + const { set } = this.context + this.$nextTick(() => { + const triggerNode = getElement(`#${this.triggerId}`) + if (!triggerNode) { + console.warn('[Chakra-ui]: Unable to locate PopoverTrigger node') + } else { + set('triggerNode', triggerNode) + } + }) + }, + render (h) { + let clone + const children = this.$slots.default.filter(e => e.tag) + if (!children) return console.error('[Chakra-ui]: Popover Trigger expects at least one child') + if (children.length && children.length > 1) return console.error('[Chakra-ui]: Popover Trigger can only have a single child element') + const cloned = cloneVNode(children[0], h) + + const { isOpen, popoverId } = this.context + + clone = h(cloned.componentOptions.Ctor, { + ...cloned.data, + ...(cloned.componentOptions.listeners || {}), + props: { + ...(cloned.data.props || {}), + ...cloned.componentOptions.propsData + }, + attrs: { + id: this.triggerId, + 'aria-haspopup': 'dialog', + 'aria-expanded': isOpen, + 'aria-controls': popoverId + }, + nativeOn: this.eventHandlers + }, cloned.componentOptions.children) + + return clone + } +} + +const PopoverContent = { + name: 'PopoverContent', + inject: ['$PopoverContext', '$colorMode'], + props: { + ...styleProps, + gutter: { + type: [Number, String], + default: 4 + }, + ariaLabel: String + }, + computed: { + context () { + return this.$PopoverContext() + }, + contentId () { + return `popover-content-${useId()}` + }, + colorMode () { + return this.$colorMode() + }, + eventHandlers () { + const { trigger, handleBlur, closePopover, closeOnEscape } = this.context + + let eventHandlers = {} + + if (trigger === 'click') { + eventHandlers = { + blur: (e) => { + this.$emit('blur', e) + handleBlur(e) + } + } + } + + if (trigger === 'hover') { + eventHandlers = { + ...eventHandlers, + mouseenter: (e) => { + this.$emit('mouseenter', e) + this.context.set('isHovering', true) + setTimeout(this.context.openPopover(), 300) + }, + mouseleave: (e) => { + this.$emit('mouseleave', e) + this.context.set('isHovering', false) + setTimeout(() => { + if (this.context.isHovering === false) { + this.context.closePopover() + } + }, 300) + } + } + } + + eventHandlers = { + ...eventHandlers, + keydown: (e) => { + this.$emit('keydown', e) + if (e.key === 'Escape' && closeOnEscape) { + closePopover && closePopover() + } + } + } + + return eventHandlers + }, + calculatedAttrs () { + const { trigger } = this.context + if (trigger === 'click') { + return { + role: 'dialog', + 'aria-modal': 'false' + } + } + + if (trigger === 'hover') { + return { + role: 'tooltip' + } + } + } + }, + mounted () { + const { set, popoverId } = this.context + this.$nextTick(() => { + const contentNode = getElement(`#${popoverId}`) + if (!contentNode) { + console.warn('[Chakra-ui]: Unable to locate PopoverContent node') + } else { + set('contentNode', contentNode) + } + }) + }, + render (h) { + const { isOpen, triggerNode, popoverId, usePortal, placement } = this.context + const bg = this.colorMode === 'light' ? 'white' : 'gray.700' + + return h(Popper, { + props: { + ...forwardProps(this.$props), + as: 'section', + usePortal: usePortal, + isOpen, + placement, + anchorEl: triggerNode, + modifiers: { offset: { enabled: true, offset: `0, ${this.gutter}` } }, + bg, + width: '100%', + position: 'relative', + display: 'flex', + flexDirection: 'column', + rounded: 'md', + shadow: 'sm', + maxWidth: 'xs', + _focus: { outline: 0, shadow: 'outline' } + }, + attrs: { + id: popoverId, + tabIndex: -1, + 'aria-labelledby': this.headerId, + 'aria-describedby': this.bodyId, + 'aria-label': this.ariaLabel, + 'aria-hidden': !isOpen, + ...this.calculatedAttrs + }, + nativeOn: this.eventHandlers + }, this.$slots.default) + } +} + +const PopoverHeader = { + name: 'PopoverHeader', + inject: ['$PopoverContext'], + props: baseProps, + computed: { + context () { + return this.$PopoverContext() + }, + headerId () { + return this.context.headerId + } + }, + render (h) { + return h(Box, { + props: { + ...forwardProps(this.$props), + as: 'header', + px: '0.75rem', + py: '0.5rem', + borderBottomWidth: '1px' + }, + attrs: { + id: this.headerId + } + }, this.$slots.default) + } +} + +const PopoverBody = { + name: 'PopoverBody', + props: baseProps, + inject: ['$PopoverContext'], + computed: { + context () { + return this.$PopoverContext() + }, + bodyId () { + return this.context.bodyId + } + }, + render (h) { + return h(Box, { + props: { + ...forwardProps(this.$props), + flex: 1, + px: '0.75rem', + py: '0.5rem' + }, + attrs: { + id: this.bodyId, + 'data-id': this.bodyId + } + }, this.$slots.default) + } +} + +const PopoverArrow = { + name: 'PopoverArrow', + props: baseProps, + render (h) { + return h(PopperArrow, { + props: forwardProps(this.$props) + }) + } +} + +const PopoverCloseButton = { + name: 'PopoverCloseButton', + inject: ['$PopoverContext'], + props: styleProps, + computed: { + context () { + return this.$PopoverContext() + } + }, + render (h) { + return h(CloseButton, { + props: { + ...forwardProps(this.$props), + size: 'sm', + pos: 'absolute', + rounded: 'md', + top: 1, + right: 2, + p: 2 + }, + on: { + click: (e) => { + this.$emit('click', e) + this.context.closePopover() + } + } + }) + } +} + +const PopoverFooter = { + name: 'PopoverFooter', + props: baseProps, + render (h) { + return h(Box, { + props: { + ...forwardProps(this.$props), + as: 'footer', + px: '0.75rem', + py: '0.5rem', + borderTopWidth: '1px' + } + }, this.$slots.default) + } +} + +export { + Popover, + PopoverTrigger, + PopoverContent, + PopoverHeader, + PopoverBody, + PopoverArrow, + PopoverCloseButton, + PopoverFooter +} diff --git a/packages/chakra-ui-core/src/Popover/index.js b/packages/chakra-ui-core/src/Popover/index.js index a37961e3..b2f899f9 100644 --- a/packages/chakra-ui-core/src/Popover/index.js +++ b/packages/chakra-ui-core/src/Popover/index.js @@ -1,586 +1 @@ -import Fragment from '../Fragment' -import { Popper, PopperArrow } from '../Popper' -import { useId, cloneVNode, getElement, isVueComponent, forwardProps } from '../utils' -import styleProps, { baseProps } from '../config/props' -import Box from '../Box' -import CloseButton from '../CloseButton' - -const Popover = { - name: 'Popover', - provide () { - return { - $PopoverContext: () => this.PopoverContext - } - }, - props: { - id: { - type: String, - default: `popover-id-${useId()}` - }, - defaultIsOpen: Boolean, - isOpen: Boolean, - returnFocusOnClose: { - type: Boolean, - default: true - }, - initialFocusRef: [Object, String], - trigger: { - type: String, - default: 'click' - }, - closeOnBlur: { - type: Boolean, - default: true - }, - closeOnEscape: { - type: Boolean, - default: true - }, - usePortal: Boolean, - placement: { - type: String, - default: 'auto' - } - }, - computed: { - PopoverContext () { - return { - set: this.set, - isOpen: this._isOpen, - closePopover: this.closePopover, - openPopover: this.openPopover, - toggleOpen: this.toggleOpen, - triggerNode: this.triggerNode, - contentNode: this.contentNode, - setTriggerNode: this.setTriggerNode, - popoverId: this.id, - trigger: this.trigger, - isHovering: this.isHovering, - handleBlur: this.handleBlur, - closeOnEscape: this.closeOnEscape, - headerId: this.headerId, - bodyId: this.bodyId, - usePortal: this.usePortal, - placement: this.placement - } - }, - isControlled () { - return this.isOpen !== false - }, - _isOpen: { - get () { - return this.isControlled ? this.isOpen : this.isOpenValue - }, - set (value) { - this.isOpenValue = value - } - }, - _initialFocusRef () { - return this.getNode(this.initialFocusRef) - }, - headerId () { - return `${this.id}-header` - }, - bodyId () { - return `${this.id}-body` - } - }, - data () { - return { - isOpenValue: this.defaultIsOpen || false, - triggerNode: undefined, - contentNode: undefined, - prevIsOpen: false, - isHovering: false - } - }, - mounted () { - /** - * The purpose of this watcher is to keep record of the previous - * isOpen value. - */ - this.$watch('_isOpen', (_newVal, oldVal) => { - this.prevIsOpen = oldVal - }, { - immediate: true - }) - - this.$watch(vm => [ - vm._isOpen, - vm._initialFocusRef, - vm.trigger, - vm.contentNode, - vm.triggerNode, - vm.prevIsOpen, - vm.returnFocusOnClose - ], () => { - if (this._isOpen && this.trigger === 'click') { - /** - * Caveat here: - * Until Vue 3 is reease, using it's $refs as props may not always return a value - * in the props unless the consumer component updates it's context. This is because - * Vue asynchronously updtaes the DOM and is also not reactive. - * - * Where this doesnt' work, we fallback to using an element selector to query - * the element from the DOM. And use it as the initial focus ref. - * - * Work-around could be to use plain old JS selectors - */ - setTimeout(() => { - if (this._initialFocusRef) { - this._initialFocusRef.focus() - } else { - if (this.contentNode) { - this.contentNode.focus() - } - } - }) - } - - if (!this._isOpen && this.prevIsOpen && this.trigger === 'click' && this.returnFocusOnClose) { - if (this.triggerNode) { - this.triggerNode.focus() - } - } - }) - }, - methods: { - /** - * Closes popover - */ - closePopover () { - if (!this.isControlled) { - this._isOpen = false - } - this.$emit('close') - }, - /** - * Opens popover - */ - openPopover () { - if (!this.isControlled) { - this._isOpen = true - } - this.$emit('open') - }, - /** - * Toggles disclosure state of popover - */ - toggleOpen () { - if (!this.isControlled) { - this._isOpen = !this._isOpen - } - - if (this._isOpen !== true) { - this.$emit('open') - } else { - this.$emit('close') - } - }, - /** - * Handles blur event - * @param {Event} e `blur` event object - */ - handleBlur (event) { - if ( - this._isOpen && - this.closeOnBlur && - this.contentNode && - this.triggerNode && - !this.contentNode.contains(event.relatedTarget) && - !this.triggerNode.contains(event.relatedTarget) - ) { - this.closePopover() - } - }, - /** - * Returns the HTML element of a Vue component or native element - * @param {Vue.Component|HTMLElement|String} element HTMLElement or Vue Component - */ - getNode (element) { - if (typeof element === 'object') { - const isVue = isVueComponent(element) - return isVue ? element.$el : element - } else if (typeof element === 'string') { - return getElement(element) - } - return null - }, - /** - * Sets the value of any component instance property. - * This function is to be passed down to context so that consumers - * can mutate context values with out doing it directly. - * Serves as a temporary fix until Vue 3 comes out - * @param {String} prop Component instance property - * @param {Any} value Property value - */ - set (prop, value) { - this[prop] = value - return this[prop] - } - }, - render (h) { - return h(Fragment, [ - this.$scopedSlots.default({ - isOpen: this._isOpen, - onClose: this.closePopover - }) - ]) - } -} - -const PopoverTrigger = { - name: 'PopoverTrigger', - inject: ['$PopoverContext'], - computed: { - triggerId () { - return `popover-trigger-${useId()}` - }, - context () { - return this.$PopoverContext() - }, - headerId () { - return this.context.headerId - }, - bodyId () { - return this.context.bodyId - }, - eventHandlers () { - const { trigger } = this.context - - if (trigger === 'click') { - return { - click: (e) => { - this.$emit('click', e) - this.context.toggleOpen() - } - } - } - - if (trigger === 'hover') { - return { - focus: (e) => { - this.$emit('focus', e) - this.context.openPopover() - }, - keydown: (e) => { - this.$emit('keydown', e) - if (e.key === 'Escape') { - setTimeout(this.context.closePopover(), 300) - } - }, - blur: (e) => { - this.$emit('blur', e) - this.context.closePopover() - }, - mouseenter: (e) => { - this.$emit('mouseenter', e) - this.context.set('isHovering', true) - setTimeout(this.context.openPopover(), 300) - }, - mouseleave: (e) => { - this.$emit('mouseleave', e) - this.context.set('isHovering', false) - setTimeout(() => { - if (this.context.isHovering === false) { - this.context.closePopover() - } - }, 300) - } - } - } - } - }, - mounted () { - const { set } = this.context - this.$nextTick(() => { - const triggerNode = getElement(`#${this.triggerId}`) - if (!triggerNode) { - console.warn('[Chakra-ui]: Unable to locate PopoverTrigger node') - } else { - set('triggerNode', triggerNode) - } - }) - }, - render (h) { - let clone - const children = this.$slots.default.filter(e => e.tag) - if (!children) return console.error('[Chakra-ui]: Popover Trigger expects at least one child') - if (children.length && children.length > 1) return console.error('[Chakra-ui]: Popover Trigger can only have a single child element') - const cloned = cloneVNode(children[0], h) - - const { isOpen, popoverId } = this.context - - clone = h(cloned.componentOptions.Ctor, { - ...cloned.data, - ...(cloned.componentOptions.listeners || {}), - props: { - ...(cloned.data.props || {}), - ...cloned.componentOptions.propsData - }, - attrs: { - id: this.triggerId, - 'aria-haspopup': 'dialog', - 'aria-expanded': isOpen, - 'aria-controls': popoverId - }, - nativeOn: this.eventHandlers - }, cloned.componentOptions.children) - - return clone - } -} - -const PopoverContent = { - name: 'PopoverContent', - inject: ['$PopoverContext', '$colorMode'], - props: { - ...styleProps, - gutter: { - type: [Number, String], - default: 4 - }, - ariaLabel: String - }, - computed: { - context () { - return this.$PopoverContext() - }, - contentId () { - return `popover-content-${useId()}` - }, - colorMode () { - return this.$colorMode() - }, - eventHandlers () { - const { trigger, handleBlur, closePopover, closeOnEscape } = this.context - - let eventHandlers = {} - - if (trigger === 'click') { - eventHandlers = { - blur: (e) => { - this.$emit('blur', e) - handleBlur(e) - } - } - } - - if (trigger === 'hover') { - eventHandlers = { - ...eventHandlers, - mouseenter: (e) => { - this.$emit('mouseenter', e) - this.context.set('isHovering', true) - setTimeout(this.context.openPopover(), 300) - }, - mouseleave: (e) => { - this.$emit('mouseleave', e) - this.context.set('isHovering', false) - setTimeout(() => { - if (this.context.isHovering === false) { - this.context.closePopover() - } - }, 300) - } - } - } - - eventHandlers = { - ...eventHandlers, - keydown: (e) => { - this.$emit('keydown', e) - if (e.key === 'Escape' && closeOnEscape) { - closePopover && closePopover() - } - } - } - - return eventHandlers - }, - calculatedAttrs () { - const { trigger } = this.context - if (trigger === 'click') { - return { - role: 'dialog', - 'aria-modal': 'false' - } - } - - if (trigger === 'hover') { - return { - role: 'tooltip' - } - } - } - }, - mounted () { - const { set, popoverId } = this.context - this.$nextTick(() => { - const contentNode = getElement(`#${popoverId}`) - if (!contentNode) { - console.warn('[Chakra-ui]: Unable to locate PopoverContent node') - } else { - set('contentNode', contentNode) - } - }) - }, - render (h) { - const { isOpen, triggerNode, popoverId, usePortal, placement } = this.context - const bg = this.colorMode === 'light' ? 'white' : 'gray.700' - - return h(Popper, { - props: { - ...forwardProps(this.$props), - as: 'section', - usePortal: usePortal, - isOpen, - placement, - anchorEl: triggerNode, - modifiers: { offset: { enabled: true, offset: `0, ${this.gutter}` } }, - bg, - width: '100%', - position: 'relative', - display: 'flex', - flexDirection: 'column', - rounded: 'md', - shadow: 'sm', - maxWidth: 'xs', - _focus: { outline: 0, shadow: 'outline' } - }, - attrs: { - id: popoverId, - tabIndex: -1, - 'aria-labelledby': this.headerId, - 'aria-describedby': this.bodyId, - 'aria-label': this.ariaLabel, - 'aria-hidden': !isOpen, - ...this.calculatedAttrs - }, - nativeOn: this.eventHandlers - }, this.$slots.default) - } -} - -const PopoverHeader = { - name: 'PopoverHeader', - inject: ['$PopoverContext'], - props: baseProps, - computed: { - context () { - return this.$PopoverContext() - }, - headerId () { - return this.context.headerId - } - }, - render (h) { - return h(Box, { - props: { - ...forwardProps(this.$props), - as: 'header', - px: '0.75rem', - py: '0.5rem', - borderBottomWidth: '1px' - }, - attrs: { - id: this.headerId - } - }, this.$slots.default) - } -} - -const PopoverBody = { - name: 'PopoverBody', - props: baseProps, - inject: ['$PopoverContext'], - computed: { - context () { - return this.$PopoverContext() - }, - bodyId () { - return this.context.bodyId - } - }, - render (h) { - return h(Box, { - props: { - ...forwardProps(this.$props), - flex: 1, - px: '0.75rem', - py: '0.5rem' - }, - attrs: { - id: this.bodyId, - 'data-id': this.bodyId - } - }, this.$slots.default) - } -} - -const PopoverArrow = { - name: 'PopoverArrow', - props: baseProps, - render (h) { - return h(PopperArrow, { - props: forwardProps(this.$props) - }) - } -} - -const PopoverCloseButton = { - name: 'PopoverCloseButton', - inject: ['$PopoverContext'], - props: styleProps, - computed: { - context () { - return this.$PopoverContext() - } - }, - render (h) { - return h(CloseButton, { - props: { - ...forwardProps(this.$props), - size: 'sm', - pos: 'absolute', - rounded: 'md', - top: 1, - right: 2, - p: 2 - }, - on: { - click: (e) => { - this.$emit('click', e) - this.context.closePopover() - } - } - }) - } -} - -const PopoverFooter = { - name: 'PopoverFooter', - props: baseProps, - render (h) { - return h(Box, { - props: { - ...forwardProps(this.$props), - as: 'footer', - px: '0.75rem', - py: '0.5rem', - borderTopWidth: '1px' - } - }, this.$slots.default) - } -} - -export { - Popover, - PopoverTrigger, - PopoverContent, - PopoverHeader, - PopoverBody, - PopoverArrow, - PopoverCloseButton, - PopoverFooter -} +export * from './Popover' diff --git a/packages/chakra-ui-core/src/Popper/Popper.js b/packages/chakra-ui-core/src/Popper/Popper.js new file mode 100644 index 00000000..570c1fbc --- /dev/null +++ b/packages/chakra-ui-core/src/Popper/Popper.js @@ -0,0 +1,279 @@ +import PopperJS from 'popper.js' +import PseudoBox from '../PseudoBox' +import ClickOutside from '../ClickOutside' +import Portal from '../Portal' +import { createChainedFunction, forwardProps, isVueComponent, canUseDOM, useId, HTMLElement } from '../utils' +import styleProps from '../config/props' +import getPopperArrowStyle from './popper.styles' +import Box from '../Box' + +/** + * Flips placement if in + * @param {string} placement + */ +function flipPlacement (placement) { + const direction = + (canUseDOM && document.body.getAttribute('dir')) || + 'ltr' + + if (direction !== 'rtl') { + return placement + } + + switch (placement) { + case 'bottom-end': + return 'bottom-start' + case 'bottom-start': + return 'bottom-end' + case 'top-end': + return 'top-start' + case 'top-start': + return 'top-end' + default: + return placement + } +} + +const Popper = { + name: 'Popper', + props: { + _id: { + type: String, + default: useId(3) + }, + as: String, + isOpen: Boolean, + placement: { + type: String, + default: 'bottom' + }, + usePortal: { + type: Boolean, + default: true + }, + onClose: { + type: Function, + default: () => null + }, + closeOnClickAway: { + type: Boolean, + default: true + }, + modifiers: { + type: Object, + default: () => {} + }, + anchorEl: HTMLElement, + eventsEnabled: { + type: Boolean, + default: true + }, + arrowSize: { + type: String, + default: '1rem' + }, + arrowShadowColor: { + type: String, + default: 'rgba(0, 0, 0, 0.1)' + }, + hasArrow: { + type: Boolean, + default: true + }, + positionFixed: Boolean, + usePortalTarget: String, + ...styleProps + }, + data () { + return { + popper: null + } + }, + watch: { + placement (newValue) { + if (this.popper) { + this.popper.options.placement = newValue + this.popper.scheduleUpdate() + } + }, + isOpen (newValue) { + if (newValue) this.handleOpen() + else this.handleClose() + } + }, + computed: { + arrowStyles () { + return getPopperArrowStyle({ + arrowSize: this.arrowSize, + arrowShadowColor: this.arrowShadowColor, + hasArrow: this.hasArrow + }) + }, + portalTarget () { + return this.usePortalTarget || `#chakra-portal-${useId(4)}` + }, + popperId () { + return `popper_${useId(4)}` + }, + rtlPlacement () { + return flipPlacement(this.placement) + }, + anchor () { + return this.getNode(this.anchorEl) + }, + reference () { + const ref = this.usePortal + // There should be a much cleaner way to do this. + // But for now this works. Should return with bigger guns. + ? canUseDOM && document.querySelector(this.portalTarget).firstChild + : this.getNode(this.$el) + return ref + } + }, + methods: { + /** + * Handles open state for Popper + */ + handleOpen () { + // Double check to make sure portal target is mounted + // If it already is mounted, Portal component will use + // the existing portal target to mount popper children + (this.usePortal && this.$refs.portalRef) && this.$refs.portalRef.mountTarget() + + if (!this.anchor || !this.reference) return + if (this.popper) { + this.popper.scheduleUpdate() + } else { + this.popper = new PopperJS(this.anchor, this.reference, { + placement: this.rtlPlacement, + modifiers: { + ...(this.usePortal && { + preventOverflow: { + boundariesElement: 'window' + } + }), + ...this.modifiers + }, + onUpdate: createChainedFunction( + this.handlePopperUpdate + ), + onCreate: createChainedFunction( + this.handlePopperCreated + ), + eventsEnabled: this.eventsEnabled, + positionFixed: this.positionFixed + }) + this.popper.scheduleUpdate() + } + }, + + /** + * Returns the HTML element of a Vue component or native element + * @param {Vue.Component|HTMLElement} element HTMLElement or Vue Component + */ + getNode (element) { + const isVue = isVueComponent(element) + return isVue ? element.$el : element + }, + + /** + * Closes Popper Element + */ + handleClose () { + if (this.popper) { + this.popper.destroy() + this.popper = null + this.$emit('popper:close', {}) + } + }, + /** + * Wrapped handler for close events + */ + wrapClose () { + if (this.popper) { + if (this.onClose) this.onClose() + this.$emit('popper:close', {}) + } + }, + + /** + * Handle's popper updates when update is called + * @param {Object} payload + */ + handlePopperUpdate (payload) { + this.$emit('popper:update', payload) + this.isOpen && this.$emit('popper:open') + }, + + /** + * Handle's popper updates when update is called + * @param {Object} payload + */ + handlePopperCreated (payload) { + this.$emit('popper:create', payload) + } + }, + render (h) { + if (this.isOpen && !this.popper) { + this.handleOpen() + } + return h(Portal, { + props: { + append: true, + target: this.portalTarget, + disabled: !this.usePortal, + slim: true, + unmountOnDestroy: true, + targetSlim: true + }, + ref: 'portalRef' + }, [h(ClickOutside, { // TODO: Fix this close on clickaway handler. Could revert to useing directive instead + props: { + whitelist: [this.anchor], + isDisabled: !this.closeOnClickAway, + do: this.wrapClose + } + }, [h(PseudoBox, { + class: [this.arrowStyles], + style: { + display: this.isOpen ? 'unset' : 'none' + }, + attrs: { + ...this.$attrs, + id: this.$attrs.id || `chakra-${this.popperId}`, + 'data-popper-id': `chakra-${this.popperId}` + }, + scopedSlots: { + popperId: `chakra-${this.popperId}` + }, + props: { + ...forwardProps(this.$props) + }, + ref: 'handleRef' + }, this.$slots.default)])]) + } +} + +const PopperArrow = { + name: 'PopperArrow', + render (h) { + return h(Box, { + attrs: { + 'x-arrow': true, + role: 'presentation' + }, + props: { + bg: 'inherit', + ...forwardProps(this.$props) + }, + on: { + click: (e) => this.$emit('cheese', e) + } + }) + } +} + +export { + Popper, + PopperArrow +} diff --git a/packages/chakra-ui-core/src/Popper/index.js b/packages/chakra-ui-core/src/Popper/index.js index 570c1fbc..ce251932 100644 --- a/packages/chakra-ui-core/src/Popper/index.js +++ b/packages/chakra-ui-core/src/Popper/index.js @@ -1,279 +1 @@ -import PopperJS from 'popper.js' -import PseudoBox from '../PseudoBox' -import ClickOutside from '../ClickOutside' -import Portal from '../Portal' -import { createChainedFunction, forwardProps, isVueComponent, canUseDOM, useId, HTMLElement } from '../utils' -import styleProps from '../config/props' -import getPopperArrowStyle from './popper.styles' -import Box from '../Box' - -/** - * Flips placement if in - * @param {string} placement - */ -function flipPlacement (placement) { - const direction = - (canUseDOM && document.body.getAttribute('dir')) || - 'ltr' - - if (direction !== 'rtl') { - return placement - } - - switch (placement) { - case 'bottom-end': - return 'bottom-start' - case 'bottom-start': - return 'bottom-end' - case 'top-end': - return 'top-start' - case 'top-start': - return 'top-end' - default: - return placement - } -} - -const Popper = { - name: 'Popper', - props: { - _id: { - type: String, - default: useId(3) - }, - as: String, - isOpen: Boolean, - placement: { - type: String, - default: 'bottom' - }, - usePortal: { - type: Boolean, - default: true - }, - onClose: { - type: Function, - default: () => null - }, - closeOnClickAway: { - type: Boolean, - default: true - }, - modifiers: { - type: Object, - default: () => {} - }, - anchorEl: HTMLElement, - eventsEnabled: { - type: Boolean, - default: true - }, - arrowSize: { - type: String, - default: '1rem' - }, - arrowShadowColor: { - type: String, - default: 'rgba(0, 0, 0, 0.1)' - }, - hasArrow: { - type: Boolean, - default: true - }, - positionFixed: Boolean, - usePortalTarget: String, - ...styleProps - }, - data () { - return { - popper: null - } - }, - watch: { - placement (newValue) { - if (this.popper) { - this.popper.options.placement = newValue - this.popper.scheduleUpdate() - } - }, - isOpen (newValue) { - if (newValue) this.handleOpen() - else this.handleClose() - } - }, - computed: { - arrowStyles () { - return getPopperArrowStyle({ - arrowSize: this.arrowSize, - arrowShadowColor: this.arrowShadowColor, - hasArrow: this.hasArrow - }) - }, - portalTarget () { - return this.usePortalTarget || `#chakra-portal-${useId(4)}` - }, - popperId () { - return `popper_${useId(4)}` - }, - rtlPlacement () { - return flipPlacement(this.placement) - }, - anchor () { - return this.getNode(this.anchorEl) - }, - reference () { - const ref = this.usePortal - // There should be a much cleaner way to do this. - // But for now this works. Should return with bigger guns. - ? canUseDOM && document.querySelector(this.portalTarget).firstChild - : this.getNode(this.$el) - return ref - } - }, - methods: { - /** - * Handles open state for Popper - */ - handleOpen () { - // Double check to make sure portal target is mounted - // If it already is mounted, Portal component will use - // the existing portal target to mount popper children - (this.usePortal && this.$refs.portalRef) && this.$refs.portalRef.mountTarget() - - if (!this.anchor || !this.reference) return - if (this.popper) { - this.popper.scheduleUpdate() - } else { - this.popper = new PopperJS(this.anchor, this.reference, { - placement: this.rtlPlacement, - modifiers: { - ...(this.usePortal && { - preventOverflow: { - boundariesElement: 'window' - } - }), - ...this.modifiers - }, - onUpdate: createChainedFunction( - this.handlePopperUpdate - ), - onCreate: createChainedFunction( - this.handlePopperCreated - ), - eventsEnabled: this.eventsEnabled, - positionFixed: this.positionFixed - }) - this.popper.scheduleUpdate() - } - }, - - /** - * Returns the HTML element of a Vue component or native element - * @param {Vue.Component|HTMLElement} element HTMLElement or Vue Component - */ - getNode (element) { - const isVue = isVueComponent(element) - return isVue ? element.$el : element - }, - - /** - * Closes Popper Element - */ - handleClose () { - if (this.popper) { - this.popper.destroy() - this.popper = null - this.$emit('popper:close', {}) - } - }, - /** - * Wrapped handler for close events - */ - wrapClose () { - if (this.popper) { - if (this.onClose) this.onClose() - this.$emit('popper:close', {}) - } - }, - - /** - * Handle's popper updates when update is called - * @param {Object} payload - */ - handlePopperUpdate (payload) { - this.$emit('popper:update', payload) - this.isOpen && this.$emit('popper:open') - }, - - /** - * Handle's popper updates when update is called - * @param {Object} payload - */ - handlePopperCreated (payload) { - this.$emit('popper:create', payload) - } - }, - render (h) { - if (this.isOpen && !this.popper) { - this.handleOpen() - } - return h(Portal, { - props: { - append: true, - target: this.portalTarget, - disabled: !this.usePortal, - slim: true, - unmountOnDestroy: true, - targetSlim: true - }, - ref: 'portalRef' - }, [h(ClickOutside, { // TODO: Fix this close on clickaway handler. Could revert to useing directive instead - props: { - whitelist: [this.anchor], - isDisabled: !this.closeOnClickAway, - do: this.wrapClose - } - }, [h(PseudoBox, { - class: [this.arrowStyles], - style: { - display: this.isOpen ? 'unset' : 'none' - }, - attrs: { - ...this.$attrs, - id: this.$attrs.id || `chakra-${this.popperId}`, - 'data-popper-id': `chakra-${this.popperId}` - }, - scopedSlots: { - popperId: `chakra-${this.popperId}` - }, - props: { - ...forwardProps(this.$props) - }, - ref: 'handleRef' - }, this.$slots.default)])]) - } -} - -const PopperArrow = { - name: 'PopperArrow', - render (h) { - return h(Box, { - attrs: { - 'x-arrow': true, - role: 'presentation' - }, - props: { - bg: 'inherit', - ...forwardProps(this.$props) - }, - on: { - click: (e) => this.$emit('cheese', e) - } - }) - } -} - -export { - Popper, - PopperArrow -} +export * from './Popper' diff --git a/packages/chakra-ui-core/src/Portal/Portal.js b/packages/chakra-ui-core/src/Portal/Portal.js new file mode 100644 index 00000000..ace212cb --- /dev/null +++ b/packages/chakra-ui-core/src/Portal/Portal.js @@ -0,0 +1,105 @@ +import { canUseDOM, useId, getSubstringAfterChar as gs } from '../utils' +import { MountingPortal } from 'portal-vue' +import NoSsr from '../NoSsr' +/** + * Portal Component + */ +const Portal = { + name: 'Portal', + props: { + target: String, + append: Boolean, + unmountOnDestroy: Boolean, + disabled: Boolean, + name: String, + order: Number, + slim: Boolean, + bail: Boolean, + targetSlim: Boolean, + as: { + type: String, + default: 'span' + } + }, + data () { + return { + portalTarget: undefined, + targetId: undefined + } + }, + created () { + if (!this.disabled) { + this.mountTarget() + this.unmountOnDestroy && this.$once('hook:destroyed', () => { + canUseDOM && document.body.removeChild(this.portalTarget) + }) + } + }, + methods: { + /** + * @description Creates portal target node. If node doesn't exist, it is created and returned + * @param {String} target + * @returns {HTMLElement} + */ + createPortalTarget (target, tag) { + if (!canUseDOM) { + return + } + const existingPortalElement = document.querySelector(target) + + if (existingPortalElement) { + return existingPortalElement + } else { + const el = document.createElement(tag) + if (target.startsWith('#')) { + el.id = gs(target, '#') + } + if (target.startsWith('.')) { + el.classList.add(gs(target, '.')) + el.id = useId(4) + } + if (document.body != null) { + document.body.appendChild(el) + } + return el + } + }, + mountTarget () { + if (!canUseDOM) { + return + } + this.portalTarget = this.createPortalTarget(this.target, this.as) + this.targetId = this.portalTarget.id + this.$forceUpdate() // Force re-render in case of changes. + if (this.portalTarget && this.portalTarget.isConnected) { + this.$nextTick(() => { + this.$emit('portal:targetConnected') + }) + } + }, + unmountTarget () { + if (!this.disabled) { + (canUseDOM && this.portalTarget.isConnected) && document.body.removeChild(this.portalTarget) + } + } + }, + render (h) { + const children = this.$slots.default + return !this.disabled ? h(NoSsr, [ + h(MountingPortal, { + props: { + append: this.append, + mountTo: `#${this.targetId}`, + disabled: this.disabled, + name: this.name, + order: this.order, + slim: this.slim, + bail: this.bail, + targetSlim: this.targetSlim + } + }, children) + ]) : children[0] + } +} + +export default Portal diff --git a/packages/chakra-ui-core/src/Portal/index.js b/packages/chakra-ui-core/src/Portal/index.js index ace212cb..a86b528e 100644 --- a/packages/chakra-ui-core/src/Portal/index.js +++ b/packages/chakra-ui-core/src/Portal/index.js @@ -1,105 +1,2 @@ -import { canUseDOM, useId, getSubstringAfterChar as gs } from '../utils' -import { MountingPortal } from 'portal-vue' -import NoSsr from '../NoSsr' -/** - * Portal Component - */ -const Portal = { - name: 'Portal', - props: { - target: String, - append: Boolean, - unmountOnDestroy: Boolean, - disabled: Boolean, - name: String, - order: Number, - slim: Boolean, - bail: Boolean, - targetSlim: Boolean, - as: { - type: String, - default: 'span' - } - }, - data () { - return { - portalTarget: undefined, - targetId: undefined - } - }, - created () { - if (!this.disabled) { - this.mountTarget() - this.unmountOnDestroy && this.$once('hook:destroyed', () => { - canUseDOM && document.body.removeChild(this.portalTarget) - }) - } - }, - methods: { - /** - * @description Creates portal target node. If node doesn't exist, it is created and returned - * @param {String} target - * @returns {HTMLElement} - */ - createPortalTarget (target, tag) { - if (!canUseDOM) { - return - } - const existingPortalElement = document.querySelector(target) - - if (existingPortalElement) { - return existingPortalElement - } else { - const el = document.createElement(tag) - if (target.startsWith('#')) { - el.id = gs(target, '#') - } - if (target.startsWith('.')) { - el.classList.add(gs(target, '.')) - el.id = useId(4) - } - if (document.body != null) { - document.body.appendChild(el) - } - return el - } - }, - mountTarget () { - if (!canUseDOM) { - return - } - this.portalTarget = this.createPortalTarget(this.target, this.as) - this.targetId = this.portalTarget.id - this.$forceUpdate() // Force re-render in case of changes. - if (this.portalTarget && this.portalTarget.isConnected) { - this.$nextTick(() => { - this.$emit('portal:targetConnected') - }) - } - }, - unmountTarget () { - if (!this.disabled) { - (canUseDOM && this.portalTarget.isConnected) && document.body.removeChild(this.portalTarget) - } - } - }, - render (h) { - const children = this.$slots.default - return !this.disabled ? h(NoSsr, [ - h(MountingPortal, { - props: { - append: this.append, - mountTo: `#${this.targetId}`, - disabled: this.disabled, - name: this.name, - order: this.order, - slim: this.slim, - bail: this.bail, - targetSlim: this.targetSlim - } - }, children) - ]) : children[0] - } -} - +import Portal from './Portal' export default Portal diff --git a/packages/chakra-ui-core/src/Progress/Progress.js b/packages/chakra-ui-core/src/Progress/Progress.js new file mode 100644 index 00000000..ec980677 --- /dev/null +++ b/packages/chakra-ui-core/src/Progress/Progress.js @@ -0,0 +1,188 @@ +import Box from '../Box' +import { generateStripe, valueToPercent, forwardProps } from '../utils' +import { css, keyframes } from 'emotion' +import { baseProps } from '../config/props' + +const stripe = keyframes({ + from: { backgroundPosition: '1rem 0' }, + to: { backgroundPosition: '0 0' } +}) + +const stripeAnimation = css({ + animation: `${stripe} 1s linear infinite` +}) + +const progressbarSizes = { + lg: '1rem', + md: '0.75rem', + sm: '0.5rem' +} + +const ProgressLabel = { + name: 'ProgressLabel', + props: baseProps, + render (h) { + return h(Box, { + props: { + textAlign: 'center', + width: '100%', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const ProgressTrack = { + name: 'ProgressTrack', + props: { + ...baseProps, + size: [String, Number, Array] + }, + render (h) { + return h(Box, { + props: { + pos: 'relative', + height: progressbarSizes[this.size || 'md'], + overflow: 'hidden', + w: '100%', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const ProgressIndicator = { + name: 'ProgressIndicator', + props: { + ...baseProps, + isIndeterminate: Boolean, + min: Number, + max: Number, + value: Number + }, + computed: { + percent () { + return valueToPercent(this.value, this.min, this.max) + } + }, + render (h) { + return h(Box, { + props: { + height: '100%', + transition: 'all 0.3s', + width: `${this.percent}%`, + ...forwardProps(this.$props) + }, + attrs: { + 'aria-valuemax': this.max, + 'aria-valuemin': this.min, + 'aria-valuenow': this.isIndeterminate ? null : this.value, + 'role': 'progressbar' + } + }, this.$slots.default) + } +} + +const Progress = { + name: 'Progress', + inject: ['$colorMode'], + props: { + ...baseProps, + color: { + type: String, + default: 'blue' + }, + value: { + type: Number, + default: 63 + }, + min: { + type: Number, + default: 0 + }, + max: { + type: Number, + default: 100 + }, + size: { + type: [String, Array], + default: 'md' + }, + hasStripe: Boolean, + isAnimated: Boolean, + borderRadius: [String, Array, Number], + rounded: [String, Array], + isIndeterminate: Boolean + }, + computed: { + colorMode () { + return this.$colorMode() + }, + trackColor () { + return { light: 'gray.100', dark: 'whiteAlpha.300' } + }, + indicatorColor () { + return { light: `${this.color}.500`, dark: `${this.color}.200` } + }, + stripeStyle () { + return { + light: generateStripe({}), + dark: generateStripe({ + color: 'rgba(0,0,0,0.1)' + }) + } + }, + __borderRadius () { + return this.rounded || this.borderRadius + } + }, + render (h) { + const _borderRadius = this.rounded || this.borderRadius + + const trackColor = { light: 'gray.100', dark: 'whiteAlpha.300' } + const indicatorColor = { light: `${this.color}.500`, dark: `${this.color}.200` } + + const stripeStyle = { + light: generateStripe({}), + dark: generateStripe({ + color: 'rgba(0,0,0,0.1)' + }) + } + + return h(ProgressTrack, { + props: { + size: this.size, + bg: trackColor[this.colorMode], + borderRadius: _borderRadius, + ...forwardProps(this.$props) + } + }, [ + h(ProgressIndicator, { + class: [ + this.hasStripe && stripeStyle[this.colorMode], + this.hasStripe && this.isAnimated && stripeAnimation + ], + props: { + min: this.min, + max: this.max, + value: this.value, + bg: indicatorColor[this.colorMode], + borderRadius: this.__borderRadius, + ...this.isIndeterminate && { + width: '100%', + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + willChange: 'left, right' + } + } + }, this.$slots.default) + ]) + } +} + +export { + Progress, + ProgressLabel +} diff --git a/packages/chakra-ui-core/src/Progress/index.js b/packages/chakra-ui-core/src/Progress/index.js index ec980677..ec6a4f15 100644 --- a/packages/chakra-ui-core/src/Progress/index.js +++ b/packages/chakra-ui-core/src/Progress/index.js @@ -1,188 +1 @@ -import Box from '../Box' -import { generateStripe, valueToPercent, forwardProps } from '../utils' -import { css, keyframes } from 'emotion' -import { baseProps } from '../config/props' - -const stripe = keyframes({ - from: { backgroundPosition: '1rem 0' }, - to: { backgroundPosition: '0 0' } -}) - -const stripeAnimation = css({ - animation: `${stripe} 1s linear infinite` -}) - -const progressbarSizes = { - lg: '1rem', - md: '0.75rem', - sm: '0.5rem' -} - -const ProgressLabel = { - name: 'ProgressLabel', - props: baseProps, - render (h) { - return h(Box, { - props: { - textAlign: 'center', - width: '100%', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const ProgressTrack = { - name: 'ProgressTrack', - props: { - ...baseProps, - size: [String, Number, Array] - }, - render (h) { - return h(Box, { - props: { - pos: 'relative', - height: progressbarSizes[this.size || 'md'], - overflow: 'hidden', - w: '100%', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const ProgressIndicator = { - name: 'ProgressIndicator', - props: { - ...baseProps, - isIndeterminate: Boolean, - min: Number, - max: Number, - value: Number - }, - computed: { - percent () { - return valueToPercent(this.value, this.min, this.max) - } - }, - render (h) { - return h(Box, { - props: { - height: '100%', - transition: 'all 0.3s', - width: `${this.percent}%`, - ...forwardProps(this.$props) - }, - attrs: { - 'aria-valuemax': this.max, - 'aria-valuemin': this.min, - 'aria-valuenow': this.isIndeterminate ? null : this.value, - 'role': 'progressbar' - } - }, this.$slots.default) - } -} - -const Progress = { - name: 'Progress', - inject: ['$colorMode'], - props: { - ...baseProps, - color: { - type: String, - default: 'blue' - }, - value: { - type: Number, - default: 63 - }, - min: { - type: Number, - default: 0 - }, - max: { - type: Number, - default: 100 - }, - size: { - type: [String, Array], - default: 'md' - }, - hasStripe: Boolean, - isAnimated: Boolean, - borderRadius: [String, Array, Number], - rounded: [String, Array], - isIndeterminate: Boolean - }, - computed: { - colorMode () { - return this.$colorMode() - }, - trackColor () { - return { light: 'gray.100', dark: 'whiteAlpha.300' } - }, - indicatorColor () { - return { light: `${this.color}.500`, dark: `${this.color}.200` } - }, - stripeStyle () { - return { - light: generateStripe({}), - dark: generateStripe({ - color: 'rgba(0,0,0,0.1)' - }) - } - }, - __borderRadius () { - return this.rounded || this.borderRadius - } - }, - render (h) { - const _borderRadius = this.rounded || this.borderRadius - - const trackColor = { light: 'gray.100', dark: 'whiteAlpha.300' } - const indicatorColor = { light: `${this.color}.500`, dark: `${this.color}.200` } - - const stripeStyle = { - light: generateStripe({}), - dark: generateStripe({ - color: 'rgba(0,0,0,0.1)' - }) - } - - return h(ProgressTrack, { - props: { - size: this.size, - bg: trackColor[this.colorMode], - borderRadius: _borderRadius, - ...forwardProps(this.$props) - } - }, [ - h(ProgressIndicator, { - class: [ - this.hasStripe && stripeStyle[this.colorMode], - this.hasStripe && this.isAnimated && stripeAnimation - ], - props: { - min: this.min, - max: this.max, - value: this.value, - bg: indicatorColor[this.colorMode], - borderRadius: this.__borderRadius, - ...this.isIndeterminate && { - width: '100%', - position: 'absolute', - top: 0, - left: 0, - bottom: 0, - willChange: 'left, right' - } - } - }, this.$slots.default) - ]) - } -} - -export { - Progress, - ProgressLabel -} +export * from './Progress' diff --git a/packages/chakra-ui-core/src/PseudoBox/PseudoBox.js b/packages/chakra-ui-core/src/PseudoBox/PseudoBox.js new file mode 100644 index 00000000..35e0003c --- /dev/null +++ b/packages/chakra-ui-core/src/PseudoBox/PseudoBox.js @@ -0,0 +1,55 @@ +import { css } from 'emotion' +import __css from '@styled-system/css' +import { background, border, color, borderRadius, flexbox, grid, layout, position, shadow, space, typography, compose } from 'styled-system' +import Box from '../Box' +import styleProps, { propsConfig } from '../config/props' +import { parsePseudoStyles } from './utils' + +const systemProps = compose( + layout, + color, + space, + background, + border, + borderRadius, + grid, + position, + shadow, + typography, + flexbox, + propsConfig +) + +const PseudoBox = { + name: 'PseudoBox', + inject: ['$theme'], + props: { + as: { + type: [String, Object], + default: () => 'div' + }, + to: [String, Object], + ...styleProps + }, + computed: { + theme () { + return this.$theme() + } + }, + render (h) { + const { as, to, ...cleanedStyleProps } = this.$props + const { pseudoStyles, baseProps } = parsePseudoStyles(cleanedStyleProps) + const baseStyles = systemProps({ ...baseProps, theme: this.theme }) + const _pseudoStyles = __css(pseudoStyles)(this.theme) + + return h(Box, { + class: css({ ...baseStyles, ..._pseudoStyles }), + props: { + as, + to + } + }, this.$slots.default) + } +} + +export default PseudoBox diff --git a/packages/chakra-ui-core/src/PseudoBox/index.js b/packages/chakra-ui-core/src/PseudoBox/index.js index 35e0003c..af8a7348 100644 --- a/packages/chakra-ui-core/src/PseudoBox/index.js +++ b/packages/chakra-ui-core/src/PseudoBox/index.js @@ -1,55 +1,2 @@ -import { css } from 'emotion' -import __css from '@styled-system/css' -import { background, border, color, borderRadius, flexbox, grid, layout, position, shadow, space, typography, compose } from 'styled-system' -import Box from '../Box' -import styleProps, { propsConfig } from '../config/props' -import { parsePseudoStyles } from './utils' - -const systemProps = compose( - layout, - color, - space, - background, - border, - borderRadius, - grid, - position, - shadow, - typography, - flexbox, - propsConfig -) - -const PseudoBox = { - name: 'PseudoBox', - inject: ['$theme'], - props: { - as: { - type: [String, Object], - default: () => 'div' - }, - to: [String, Object], - ...styleProps - }, - computed: { - theme () { - return this.$theme() - } - }, - render (h) { - const { as, to, ...cleanedStyleProps } = this.$props - const { pseudoStyles, baseProps } = parsePseudoStyles(cleanedStyleProps) - const baseStyles = systemProps({ ...baseProps, theme: this.theme }) - const _pseudoStyles = __css(pseudoStyles)(this.theme) - - return h(Box, { - class: css({ ...baseStyles, ..._pseudoStyles }), - props: { - as, - to - } - }, this.$slots.default) - } -} - +import PseudoBox from './PseudoBox' export default PseudoBox diff --git a/packages/chakra-ui-core/src/Radio/Radio.js b/packages/chakra-ui-core/src/Radio/Radio.js new file mode 100644 index 00000000..154f2ccd --- /dev/null +++ b/packages/chakra-ui-core/src/Radio/Radio.js @@ -0,0 +1,121 @@ +import styleProps from '../config/props' +import { useVariantColorWarning, forwardProps } from '../utils' +import useCheckboxStyle from '../Checkbox/checkbox.styles' +import Box from '../Box' +import VisuallyHidden from '../VisuallyHidden' +import ControlBox from '../ControlBox' + +const Radio = { + name: 'Radio', + inject: ['$colorMode', '$theme'], + props: { + ...styleProps, + id: String, + name: String, + value: String, + _ariaLabel: String, + _ariaLabelledBy: String, + variantColor: { + type: String, + default: 'blue' + }, + defaultIsChecked: Boolean, + isChecked: Boolean, + isFullWidth: Boolean, + size: { + type: String, + default: 'md' + }, + isDisabled: Boolean, + isInvalid: Boolean + }, + computed: { + colorMode () { + return this.$colorMode() + }, + theme () { + return this.$theme() + }, + radioStyles () { + useVariantColorWarning(this.theme, 'Radio', this.variantColor) + return useCheckboxStyle({ + color: this.variantColor, + size: this.size, + colorMode: this.colorMode, + type: 'radio' + }) + } + }, + render (h) { + const children = this.$slots.default + + return h(Box, { + props: { + ...forwardProps(this.$props), + as: 'label', + display: 'inline-flex', + verticalAlign: 'top', + alignItems: 'center', + width: this.isFullWidth ? 'full' : undefined, + cursor: this.isDisabled ? 'not-allowed' : 'pointer' + }, + attrs: { + for: this.id + } + }, [ + h(VisuallyHidden, { + props: { + as: 'input' + }, + domProps: { + defaultChecked: this.defaultIsChecked + }, + attrs: { + type: 'radio', + 'aria-label': this._ariaLabel, + 'aria-labelledby': this._ariaLabelledBy, + id: this.id, + name: this.name, + value: this.value, + 'aria-invalid': this.isInvalid, + disabled: this.isDisabled, + 'aria-disabled': this.isDisabled + }, + nativeOn: { + change: (e) => this.$emit('change', e) + } + }), + h(ControlBox, { + props: { + ...forwardProps(this.$props), + ...this.radioStyles, + rounded: 'full' + }, + attrs: { + type: 'radio' + } + }, [ + h(Box, { + props: { + as: 'span', + bg: 'currentColor', + rounded: 'full', + w: '50%', + h: '50%' + } + }) + ]), + children && h(Box, { + props: { + ml: 2, + fontSize: this.size, + fontFamily: 'body', + userSelect: 'none', + opacity: this.isDisabled ? 0.32 : 1 + } + }, children) + ]) + } +} + +export default Radio diff --git a/packages/chakra-ui-core/src/Radio/index.js b/packages/chakra-ui-core/src/Radio/index.js index 154f2ccd..5eb11066 100644 --- a/packages/chakra-ui-core/src/Radio/index.js +++ b/packages/chakra-ui-core/src/Radio/index.js @@ -1,121 +1,2 @@ -import styleProps from '../config/props' -import { useVariantColorWarning, forwardProps } from '../utils' -import useCheckboxStyle from '../Checkbox/checkbox.styles' -import Box from '../Box' -import VisuallyHidden from '../VisuallyHidden' -import ControlBox from '../ControlBox' - -const Radio = { - name: 'Radio', - inject: ['$colorMode', '$theme'], - props: { - ...styleProps, - id: String, - name: String, - value: String, - _ariaLabel: String, - _ariaLabelledBy: String, - variantColor: { - type: String, - default: 'blue' - }, - defaultIsChecked: Boolean, - isChecked: Boolean, - isFullWidth: Boolean, - size: { - type: String, - default: 'md' - }, - isDisabled: Boolean, - isInvalid: Boolean - }, - computed: { - colorMode () { - return this.$colorMode() - }, - theme () { - return this.$theme() - }, - radioStyles () { - useVariantColorWarning(this.theme, 'Radio', this.variantColor) - return useCheckboxStyle({ - color: this.variantColor, - size: this.size, - colorMode: this.colorMode, - type: 'radio' - }) - } - }, - render (h) { - const children = this.$slots.default - - return h(Box, { - props: { - ...forwardProps(this.$props), - as: 'label', - display: 'inline-flex', - verticalAlign: 'top', - alignItems: 'center', - width: this.isFullWidth ? 'full' : undefined, - cursor: this.isDisabled ? 'not-allowed' : 'pointer' - }, - attrs: { - for: this.id - } - }, [ - h(VisuallyHidden, { - props: { - as: 'input' - }, - domProps: { - defaultChecked: this.defaultIsChecked - }, - attrs: { - type: 'radio', - 'aria-label': this._ariaLabel, - 'aria-labelledby': this._ariaLabelledBy, - id: this.id, - name: this.name, - value: this.value, - 'aria-invalid': this.isInvalid, - disabled: this.isDisabled, - 'aria-disabled': this.isDisabled - }, - nativeOn: { - change: (e) => this.$emit('change', e) - } - }), - h(ControlBox, { - props: { - ...forwardProps(this.$props), - ...this.radioStyles, - rounded: 'full' - }, - attrs: { - type: 'radio' - } - }, [ - h(Box, { - props: { - as: 'span', - bg: 'currentColor', - rounded: 'full', - w: '50%', - h: '50%' - } - }) - ]), - children && h(Box, { - props: { - ml: 2, - fontSize: this.size, - fontFamily: 'body', - userSelect: 'none', - opacity: this.isDisabled ? 0.32 : 1 - } - }, children) - ]) - } -} - +import Radio from './Radio' export default Radio diff --git a/packages/chakra-ui-core/src/RadioButtonGroup/RadioButtonGroup.js b/packages/chakra-ui-core/src/RadioButtonGroup/RadioButtonGroup.js new file mode 100644 index 00000000..72962501 --- /dev/null +++ b/packages/chakra-ui-core/src/RadioButtonGroup/RadioButtonGroup.js @@ -0,0 +1,166 @@ +import Box from '../Box' +import { baseProps } from '../config' +import { StringNumber, SNA } from '../config/props/props.types' +import { isDef, useId, cloneVNodeElement, forwardProps, cleanChildren } from '../utils' + +const RadioButtonGroup = { + name: 'RadioButtonGroup', + props: { + ...baseProps, + name: { + type: String, + default: `radio-${useId()}` + }, + defaultValue: { + type: StringNumber, + default: null + }, + value: StringNumber, + spacing: { + type: SNA, + default: '12px' + }, + isInline: Boolean + }, + data () { + return { + innerValue: this.defaultValue || null, + focusableValues: [], + allValues: [] + } + }, + computed: { + isControlled () { + return isDef(this.value) + }, + _value: { + get () { + return this.isControlled ? this.value : this.innerValue + }, + set (val) { + this.innerValue = val + } + } + }, + mounted () { + const children = cleanChildren(this.$slots.default) + this.focusableValues = children.map(child => child.componentOptions.propsData.isDisabled === true + ? null + : child.componentOptions.propsData.value) + .filter(val => isDef(val)) + + this.allValues = children.map(vnode => vnode.componentOptions.propsData.value) + }, + methods: { + /** + * Updates the current selected index + * @param {Number} index + */ + updateIndex (index) { + const childValue = this.focusableValues[index] + const _index = this.allValues.indexOf(childValue) + this.allNodes.current[_index].focus() + + if (!this.isControlled) { + this.innerValue = childValue + } + this.$emit('change', childValue) + }, + /** + * Handle keydown event + * @param {Event} event Event object + */ + handleKeyDown (event) { + if (event.key === 'Tab') { + return + } + + event.preventDefault() + + const count = this.focusableValues.length + let enabledCheckedIndex = this.focusableValues.indexOf(this._value) + + if (enabledCheckedIndex === -1) { + enabledCheckedIndex = 0 + } + + switch (event.key) { + case 'ArrowRight': + case 'ArrowDown': { + const nextIndex = (enabledCheckedIndex + 1) % count + this.updateIndex(nextIndex) + break + } + case 'ArrowLeft': + case 'ArrowUp': { + const nextIndex = (enabledCheckedIndex - 1 + count) % count + this.updateIndex(nextIndex) + break + } + default: + break + } + } + }, + render (h) { + const children = this.$slots.default + if (!children) { + console.warn(` + [Chakra-ui]: The component expects at least one child. + `) + return + } + const _this = this + const clones = children + .filter(vnode => isDef(vnode.tag)) + .map((vnode, index) => { + const isLastChild = children.length === index + 1 + const isFirstChild = index === 0 + const props = vnode.componentOptions.propsData + const spacingProps = _this.isInline ? { mr: _this.spacing } : { mb: _this.spacing } + + const isChecked = props.value === this._value + + const handleClick = () => { + if (!_this.isControlled) { + _this.innerValue = props.value + } + _this.$emit('change', props.value) + } + + const getTabIndex = () => { + // If a RadioGroup has no radio selected the first enabled radio should be focusable + if (_this._value == null) { + return isFirstChild ? 0 : -1 + } else { + return isChecked ? 0 : -1 + } + } + + return cloneVNodeElement(vnode, { + props: { + name: _this.name, + isChecked, + ...(!isLastChild && spacingProps) + }, + attrs: { + tabIndex: getTabIndex() + }, + nativeOn: { + click: handleClick + } + }, h) + }) + return h(Box, { + props: forwardProps(this.$props), + attrs: { + role: 'radiogroup' + }, + nativeOn: { + keydown: this.handleKeyDown + } + }, clones) + } +} + +export default RadioButtonGroup diff --git a/packages/chakra-ui-core/src/RadioButtonGroup/index.js b/packages/chakra-ui-core/src/RadioButtonGroup/index.js index 72962501..def2dc79 100644 --- a/packages/chakra-ui-core/src/RadioButtonGroup/index.js +++ b/packages/chakra-ui-core/src/RadioButtonGroup/index.js @@ -1,166 +1,2 @@ -import Box from '../Box' -import { baseProps } from '../config' -import { StringNumber, SNA } from '../config/props/props.types' -import { isDef, useId, cloneVNodeElement, forwardProps, cleanChildren } from '../utils' - -const RadioButtonGroup = { - name: 'RadioButtonGroup', - props: { - ...baseProps, - name: { - type: String, - default: `radio-${useId()}` - }, - defaultValue: { - type: StringNumber, - default: null - }, - value: StringNumber, - spacing: { - type: SNA, - default: '12px' - }, - isInline: Boolean - }, - data () { - return { - innerValue: this.defaultValue || null, - focusableValues: [], - allValues: [] - } - }, - computed: { - isControlled () { - return isDef(this.value) - }, - _value: { - get () { - return this.isControlled ? this.value : this.innerValue - }, - set (val) { - this.innerValue = val - } - } - }, - mounted () { - const children = cleanChildren(this.$slots.default) - this.focusableValues = children.map(child => child.componentOptions.propsData.isDisabled === true - ? null - : child.componentOptions.propsData.value) - .filter(val => isDef(val)) - - this.allValues = children.map(vnode => vnode.componentOptions.propsData.value) - }, - methods: { - /** - * Updates the current selected index - * @param {Number} index - */ - updateIndex (index) { - const childValue = this.focusableValues[index] - const _index = this.allValues.indexOf(childValue) - this.allNodes.current[_index].focus() - - if (!this.isControlled) { - this.innerValue = childValue - } - this.$emit('change', childValue) - }, - /** - * Handle keydown event - * @param {Event} event Event object - */ - handleKeyDown (event) { - if (event.key === 'Tab') { - return - } - - event.preventDefault() - - const count = this.focusableValues.length - let enabledCheckedIndex = this.focusableValues.indexOf(this._value) - - if (enabledCheckedIndex === -1) { - enabledCheckedIndex = 0 - } - - switch (event.key) { - case 'ArrowRight': - case 'ArrowDown': { - const nextIndex = (enabledCheckedIndex + 1) % count - this.updateIndex(nextIndex) - break - } - case 'ArrowLeft': - case 'ArrowUp': { - const nextIndex = (enabledCheckedIndex - 1 + count) % count - this.updateIndex(nextIndex) - break - } - default: - break - } - } - }, - render (h) { - const children = this.$slots.default - if (!children) { - console.warn(` - [Chakra-ui]: The component expects at least one child. - `) - return - } - const _this = this - const clones = children - .filter(vnode => isDef(vnode.tag)) - .map((vnode, index) => { - const isLastChild = children.length === index + 1 - const isFirstChild = index === 0 - const props = vnode.componentOptions.propsData - const spacingProps = _this.isInline ? { mr: _this.spacing } : { mb: _this.spacing } - - const isChecked = props.value === this._value - - const handleClick = () => { - if (!_this.isControlled) { - _this.innerValue = props.value - } - _this.$emit('change', props.value) - } - - const getTabIndex = () => { - // If a RadioGroup has no radio selected the first enabled radio should be focusable - if (_this._value == null) { - return isFirstChild ? 0 : -1 - } else { - return isChecked ? 0 : -1 - } - } - - return cloneVNodeElement(vnode, { - props: { - name: _this.name, - isChecked, - ...(!isLastChild && spacingProps) - }, - attrs: { - tabIndex: getTabIndex() - }, - nativeOn: { - click: handleClick - } - }, h) - }) - return h(Box, { - props: forwardProps(this.$props), - attrs: { - role: 'radiogroup' - }, - nativeOn: { - keydown: this.handleKeyDown - } - }, clones) - } -} - +import RadioButtonGroup from './RadioButtonGroup' export default RadioButtonGroup diff --git a/packages/chakra-ui-core/src/RadioGroup/RadioGroup.js b/packages/chakra-ui-core/src/RadioGroup/RadioGroup.js new file mode 100644 index 00000000..8a4dcdf6 --- /dev/null +++ b/packages/chakra-ui-core/src/RadioGroup/RadioGroup.js @@ -0,0 +1,120 @@ +import Box from '../Box' +import { baseProps } from '../config' +import { StringNumber } from '../config/props/props.types' +import { useId, cloneVNodeElement, forwardProps } from '../utils' + +const RadioGroup = { + name: 'RadioGroup', + props: { + ...baseProps, + name: { + type: String, + default: `radio-${useId()}` + }, + variantColor: String, + size: String, + defaultValue: { + type: StringNumber, + default: null + }, + isInline: Boolean, + value: { + type: StringNumber, + default: null + }, + spacing: { + type: Number, + default: 2 + } + }, + data () { + return { + innerValue: this.defaultValue || null + } + }, + computed: { + isControlled () { + return this.defaultValue != null + }, + _value: { + get () { + return this.isControlled ? this.value : this.innerValue + }, + set (val) { + this.innerValue = val + } + } + }, + methods: { + /** + * Handles event changes in radio group + * @param {Event} event Event object + */ + handleChange (event) { + if (!this.isControlled) { + this.innerValue = event.target.value + } + this.$emit('change', event.target.value) + }, + /** + * Focuses the selected option or first enabled option + */ + focus () { + const rootRef = this.$refs.radioGroup.$el + rootRef.focus = () => { + let input = rootRef.querySelector( + 'input:not(:disabled):checked' + ) + + if (!input) { + input = rootRef.querySelector('input:not(:disabled)') + } + + if (input) { + this.$nextTick(() => { + input.focus() + }) + } + } + } + }, + render (h) { + const children = this.$slots.default + + const clones = children.map((vnode, index) => { + if (!vnode.tag) return + + const isLastRadio = children.length === index + 1 + const spacingProps = this.isInline ? { mr: this.spacing } : { mb: this.spacing } + + const clone = cloneVNodeElement(vnode, { + props: { + size: vnode.componentOptions.propsData.size || this.size, + variantColor: vnode.componentOptions.propsData.variantColor || this.variantColor, + name: this.name, + isChecked: vnode.componentOptions.propsData.value === this.value + }, + on: { + change: (e) => this.handleChange(e) + } + }, h) + + return h(Box, { + props: { + display: this.isInline ? 'inline-block' : 'block', + ...(!isLastRadio && spacingProps) + } + }, [clone]) + }) + + return h(Box, { + props: forwardProps(this.$props), + attrs: { + role: 'radiogroup' + }, + ref: 'radioGroup' + }, clones) + } +} + +export default RadioGroup diff --git a/packages/chakra-ui-core/src/RadioGroup/index.js b/packages/chakra-ui-core/src/RadioGroup/index.js index 8a4dcdf6..bdc25272 100644 --- a/packages/chakra-ui-core/src/RadioGroup/index.js +++ b/packages/chakra-ui-core/src/RadioGroup/index.js @@ -1,120 +1,2 @@ -import Box from '../Box' -import { baseProps } from '../config' -import { StringNumber } from '../config/props/props.types' -import { useId, cloneVNodeElement, forwardProps } from '../utils' - -const RadioGroup = { - name: 'RadioGroup', - props: { - ...baseProps, - name: { - type: String, - default: `radio-${useId()}` - }, - variantColor: String, - size: String, - defaultValue: { - type: StringNumber, - default: null - }, - isInline: Boolean, - value: { - type: StringNumber, - default: null - }, - spacing: { - type: Number, - default: 2 - } - }, - data () { - return { - innerValue: this.defaultValue || null - } - }, - computed: { - isControlled () { - return this.defaultValue != null - }, - _value: { - get () { - return this.isControlled ? this.value : this.innerValue - }, - set (val) { - this.innerValue = val - } - } - }, - methods: { - /** - * Handles event changes in radio group - * @param {Event} event Event object - */ - handleChange (event) { - if (!this.isControlled) { - this.innerValue = event.target.value - } - this.$emit('change', event.target.value) - }, - /** - * Focuses the selected option or first enabled option - */ - focus () { - const rootRef = this.$refs.radioGroup.$el - rootRef.focus = () => { - let input = rootRef.querySelector( - 'input:not(:disabled):checked' - ) - - if (!input) { - input = rootRef.querySelector('input:not(:disabled)') - } - - if (input) { - this.$nextTick(() => { - input.focus() - }) - } - } - } - }, - render (h) { - const children = this.$slots.default - - const clones = children.map((vnode, index) => { - if (!vnode.tag) return - - const isLastRadio = children.length === index + 1 - const spacingProps = this.isInline ? { mr: this.spacing } : { mb: this.spacing } - - const clone = cloneVNodeElement(vnode, { - props: { - size: vnode.componentOptions.propsData.size || this.size, - variantColor: vnode.componentOptions.propsData.variantColor || this.variantColor, - name: this.name, - isChecked: vnode.componentOptions.propsData.value === this.value - }, - on: { - change: (e) => this.handleChange(e) - } - }, h) - - return h(Box, { - props: { - display: this.isInline ? 'inline-block' : 'block', - ...(!isLastRadio && spacingProps) - } - }, [clone]) - }) - - return h(Box, { - props: forwardProps(this.$props), - attrs: { - role: 'radiogroup' - }, - ref: 'radioGroup' - }, clones) - } -} - +import RadioGroup from './RadioGroup' export default RadioGroup diff --git a/packages/chakra-ui-core/src/Select/Select.js b/packages/chakra-ui-core/src/Select/Select.js new file mode 100644 index 00000000..74d40564 --- /dev/null +++ b/packages/chakra-ui-core/src/Select/Select.js @@ -0,0 +1,160 @@ +import { baseProps } from '../config' +import Box from '../Box' +import styleProps from '../config/props' +import { inputProps } from '../Input/input.props' +import Input from '../Input' +import splitProps from './select.utils' +import Icon from '../Icon' + +/** + * SelectIconWrapper component + */ +const SelectIconWrapper = { + name: 'SelectIconWrapper', + props: baseProps, + render (h) { + return h(Box, { + props: { + ...this.$props, + position: 'absolute', + display: 'inline-flex', + width: '1.5rem', + height: '100%', + alignItems: 'center', + justifyContent: 'center', + right: '0.5rem', + top: '50%', + pointerEvents: 'none', + zIndex: 2, + transform: 'translateY(-50%)' + } + }, this.$slots.default) + } +} + +const SelectInput = { + name: 'SelectInput', + props: { + ...styleProps, + ...inputProps, + placeholder: String, + value: String + }, + render (h) { + return h(Input, { + props: { + ...this.$props, + as: 'select', + appearance: 'none', + pr: '2rem', + pb: 'px', + lineHeight: 'normal' + }, + on: { + change: (e) => this.$emit('change', e) + }, + domProps: { + value: this.value + } + }, [ + this.placeholder && h('option', { + attrs: { + value: '' + } + }, this.placeholder), + this.$slots.default + ]) + } +} + +const Select = { + name: 'Select', + inject: ['$colorMode'], + model: { + prop: 'value', + event: 'change' + }, + props: { + ...styleProps, + ...inputProps, + rootProps: { + type: Object, + default: () => ({}) + }, + icon: String, + iconSize: { + type: Number, + default: 5 + }, + size: { + type: String, + default: 'md' + }, + isReadOnly: Boolean, + isDisabled: Boolean, + placeholder: String, + value: { + type: String, + default: '' + } + }, + computed: { + colorMode () { + return this.$colorMode() + }, + _color () { + return this.colorMode === 'dark' ? 'whiteAlpha.800' : 'inherit' + }, + _opacity () { + return this.isReadOnly || this.isDisabled ? 0.5 : null + }, + _value () { + return this.value + } + }, + render (h) { + const { rootProps, icon, iconSize, ...props } = this.$props + const [root, select] = splitProps(props) + return h(Box, { + props: { + ...root, + ...rootProps, + position: 'relative', + width: '100%' + } + }, [ + h(SelectInput, { + props: { + color: this._color, + placeholder: this.placeholder, + ...select + }, + on: { + change: (e) => this.$emit('change', e.target.value) + }, + domProps: { + value: this._value + } + }, this.$slots.default), + h(SelectIconWrapper, { + props: { + opacity: this._opacity, + color: select.color || this._color + } + }, [ + h(Icon, { + props: { + name: this.icon || 'chevron-down', + size: this.iconSize + }, + attrs: { + focusable: false, + 'aria-hidden': true + } + }) + ]) + ]) + } +} + +export default Select diff --git a/packages/chakra-ui-core/src/Select/Select.stories.js b/packages/chakra-ui-core/src/Select/Select.stories.js index 128a78c0..2368b231 100644 --- a/packages/chakra-ui-core/src/Select/Select.stories.js +++ b/packages/chakra-ui-core/src/Select/Select.stories.js @@ -1,4 +1,5 @@ import { storiesOf } from '@storybook/vue' +import { action } from '@storybook/addon-actions' import { Box, Stack, Select } from '..' storiesOf('UI | Select', module) @@ -6,13 +7,26 @@ storiesOf('UI | Select', module) components: { Box, Select }, template: ` - - ` + `, + data () { + return { + value: 'option3' + } + }, + watch: { + value (newValue) { + this.action('Selected value', newValue) + } + }, + methods: { + action: action() + } })) .add('Changing select size', () => ({ components: { Stack, Select }, diff --git a/packages/chakra-ui-core/src/Select/index.js b/packages/chakra-ui-core/src/Select/index.js index 2d41174e..ae6548b8 100644 --- a/packages/chakra-ui-core/src/Select/index.js +++ b/packages/chakra-ui-core/src/Select/index.js @@ -1,142 +1,2 @@ -import { baseProps } from '../config' -import Box from '../Box' -import styleProps from '../config/props' -import { inputProps } from '../Input/input.props' -import Input from '../Input' -import splitProps from './select.utils' -import Icon from '../Icon' - -/** - * SelectIconWrapper component - */ -const SelectIconWrapper = { - name: 'SelectIconWrapper', - props: baseProps, - render (h) { - return h(Box, { - props: { - ...this.$props, - position: 'absolute', - display: 'inline-flex', - width: '1.5rem', - height: '100%', - alignItems: 'center', - justifyContent: 'center', - right: '0.5rem', - top: '50%', - pointerEvents: 'none', - zIndex: 2, - transform: 'translateY(-50%)' - } - }, this.$slots.default) - } -} - -const SelectInput = { - name: 'SelectInput', - props: { - ...styleProps, - ...inputProps, - placeholder: String - }, - render (h) { - return h(Input, { - props: { - ...this.$props, - as: 'select', - appearance: 'none', - pr: '2rem', - pb: 'px', - lineHeight: 'normal' - }, - on: { - change: (e) => this.$emit('change', e) - } - }, [ - this.placeholder && h('option', { - attrs: { - value: '' - } - }, this.placeholder), - this.$slots.default - ]) - } -} - -const Select = { - name: 'Select', - inject: ['$colorMode'], - props: { - ...styleProps, - ...inputProps, - rootProps: { - type: Object, - default: () => ({}) - }, - icon: String, - iconSize: { - type: Number, - default: 5 - }, - size: { - type: String, - default: 'md' - }, - isReadOnly: Boolean, - isDisabled: Boolean, - placeholder: String - }, - computed: { - colorMode () { - return this.$colorMode() - }, - _color () { - return this.colorMode === 'dark' ? 'whiteAlpha.800' : 'inherit' - }, - _opacity () { - return this.isReadOnly || this.isDisabled ? 0.5 : null - } - }, - render (h) { - const { rootProps, icon, iconSize, ...props } = this.$props - const [root, select] = splitProps(props) - return h(Box, { - props: { - ...root, - ...rootProps, - position: 'relative', - width: '100%' - } - }, [ - h(SelectInput, { - props: { - color: this._color, - placeholder: this.placeholder, - ...select - }, - on: { - change: (e) => this.$emit('change', e.target.value) - } - }, this.$slots.default), - h(SelectIconWrapper, { - props: { - opacity: this._opacity, - color: select.color || this._color - } - }, [ - h(Icon, { - props: { - name: this.icon || 'chevron-down', - size: this.iconSize - }, - attrs: { - focusable: false, - 'aria-hidden': true - } - }) - ]) - ]) - } -} - +import Select from './Select' export default Select diff --git a/packages/chakra-ui-core/src/SimpleGrid/SimpleGrid.js b/packages/chakra-ui-core/src/SimpleGrid/SimpleGrid.js new file mode 100644 index 00000000..864743dd --- /dev/null +++ b/packages/chakra-ui-core/src/SimpleGrid/SimpleGrid.js @@ -0,0 +1,32 @@ +import Grid from '../Grid' +import { baseProps } from '../config/props' +import { countToColumns, widthToColumns } from './grid.styles' + +const SimpleGrid = { + name: 'SimpleGrid', + props: { + columns: [String, Number, Array], + spacingX: [String, Number, Array], + spacingY: [String, Number, Array], + spacing: [String, Number, Array], + minChildWidth: [String, Number, Array], + ...baseProps + }, + computed: { + templateColumns () { + return this.minChildWidth + ? widthToColumns(this.minChildWidth) + : countToColumns(this.columns) + } + }, + render (h) { + return h(Grid, { + gap: this.spacing, + columnGap: this.spacingX, + rowGap: this.spacingY, + templateColumns: this.templateColumns + }, this.$slots.default) + } +} + +export default SimpleGrid diff --git a/packages/chakra-ui-core/src/SimpleGrid/index.js b/packages/chakra-ui-core/src/SimpleGrid/index.js index d31f5e7f..f54bd9e9 100644 --- a/packages/chakra-ui-core/src/SimpleGrid/index.js +++ b/packages/chakra-ui-core/src/SimpleGrid/index.js @@ -1,33 +1,2 @@ -import Grid from '../Grid' -import { baseProps } from '../config/props' -import { countToColumns, widthToColumns } from './grid.styles' - -const SimpleGrid = { - name: 'SimpleGrid', - props: { - columns: [String, Number, Array], - spacingX: [String, Number, Array], - spacingY: [String, Number, Array], - spacing: [String, Number, Array], - minChildWidth: [String, Number, Array], - ...baseProps - }, - computed: { - templateColumns () { - return this.minChildWidth - ? widthToColumns(this.minChildWidth) - : countToColumns(this.columns) - } - }, - render (h) { - return h(Grid, { - gap: this.spacing, - columnGap: this.spacingX, - rowGap: this.spacingY, - templateColumns: this.templateColumns - // ...forwardProps(this.$props) - }, this.$slots.default) - } -} - +import SimpleGrid from './SimpleGrid' export default SimpleGrid diff --git a/packages/chakra-ui-core/src/Slider/Slider.js b/packages/chakra-ui-core/src/Slider/Slider.js new file mode 100644 index 00000000..6268cbf4 --- /dev/null +++ b/packages/chakra-ui-core/src/Slider/Slider.js @@ -0,0 +1,458 @@ +import { baseProps } from '../config' +import { isDef, valueToPercent, useId, getElById, forwardProps } from '../utils' +import { clampValue, roundValueToStep } from './slider.utils' +import Box from '../Box' +import useSliderStyle from './slider.styles' +import { percentToValue } from '../utils/transform' +import styleProps from '../config/props' +import PseudoBox from '../PseudoBox' + +const Slider = { + name: 'Slider', + inject: ['$theme', '$colorMode'], + props: { + ...baseProps, + value: Number, + defaultValue: Number, + isDisabled: Boolean, + max: { + type: Number, + default: 100 + }, + min: { + type: Number, + default: 0 + }, + step: { + type: Number, + default: 1 + }, + ariaLabel: String, + ariaLabelledBy: String, + ariaValueText: String, + orientation: { + type: String, + default: 'horizontal' + }, + getAriaValueText: Function, + size: { + type: String, + default: 'md' + }, + color: { + type: String, + default: 'blue' + }, + name: String, + id: String + }, + provide () { + return { + $SliderContext: () => this.SliderContext + } + }, + data () { + return { + innerValue: this.defaultValue || 0, + trackNode: undefined, + thumbNode: undefined + } + }, + computed: { + isControlled () { + return isDef(this.value) + }, + _value () { + return this.isControlled ? this.value : this.innerValue + }, + actualValue () { + return clampValue(this._value, this.min, this.max) + }, + trackPercentage () { + return valueToPercent(this.actualValue, this.min, this.max) + }, + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + }, + _id () { + return this.id || useId() + }, + trackId () { + return `slider-track-${this._id}` + }, + thumbId () { + return `slider-thumb-${this._id}` + }, + sliderStyles () { + const { rootStyle } = useSliderStyle({ + color: this.color, + colorMode: this.colorMode, + size: this.size, + theme: this.theme, + trackPercent: this.trackPercentage + }) + return rootStyle + }, + valueText () { + return this.getAriaValueText + ? this.getAriaValueText(this.actualValue) + : this.ariaValueText + }, + SliderContext () { + return { + trackNode: this.trackNode, + thumbNode: this.thumbNode, + onThumbKeyDown: this.handleThumbKeyDown, + onFocus: (e) => this.$emit('focus', e), + trackPercent: this.trackPercentage, + ariaLabelledBy: this.ariaLabelledBy, + orientation: this.orientation, + isDisabled: this.isDisabled, + size: this.size, + color: this.color, + min: this.min, + max: this.max, + valueText: this.valueText, + value: this.actualValue, + trackId: this.trackId, + thumbId: this.thumbId + } + } + }, + mounted () { + this.$nextTick(() => { + this.trackNode = getElById(this.trackId) + this.thumbNode = getElById(this.thumbId) + }) + }, + methods: { + /** + * Parses new value returned from slider change event + * @param {Event} event + */ + getNewValue (event) { + if (this.trackNode) { + const { left, width } = this.trackNode.getBoundingClientRect() + const { clientX } = event.touches ? event.touches[0] : event + + let diffX = clientX - left + let percent = diffX / width + let newValue = percentToValue(percent, this.min, this.max) + + if (this.step) { + newValue = roundValueToStep(newValue, this.step) + } + newValue = clampValue(newValue, this.min, this.max) + return newValue + } + }, + + /** + * Updates current inner value + * @param {Number} newValue New Value + */ + updateValue (newValue) { + if (!this.isControlled) { + this.innerValue = newValue + } + this.$emit('change', newValue) + }, + + /** + * Handles SliderThumb keydown event + * @param {Event} event + */ + handleThumbKeyDown (event) { + let flag = false + let newValue + const tenSteps = (this.max - this.min) / 10 + + switch (event.key) { + case 'ArrowLeft': + case 'ArrowDown': + newValue = this.actualValue - this.step + flag = true + break + case 'ArrowRight': + case 'ArrowUp': + newValue = this.actualValue + this.step + flag = true + break + case 'PageDown': + newValue = this.actualValue - tenSteps + flag = true + break + case 'PageUp': + newValue = this.actualValue + tenSteps + flag = true + break + case 'Home': + newValue = this.min + flag = true + break + case 'End': + newValue = this.max + flag = true + break + default: + return + } + + if (flag) { + event.preventDefault() + event.stopPropagation() + } + if (this.step) { + newValue = roundValueToStep(newValue, this.step) + } + newValue = clampValue(newValue, this.min, this.max) + this.updateValue(newValue) + + this.$emit('keydown', event) + }, + + /** + * Handle sliderthumb mouseup event + */ + handleMouseUp () { + document.body.removeEventListener('mousemove', this.handleMouseMove) + document.body.removeEventListener('touchmove', this.handleMouseMove) + document.body.removeEventListener('mouseup', this.handleMouseUp) + document.body.removeEventListener('touchend', this.handleMouseUp) + }, + + /** + * Handles mousedown event for slider + * @param {Event} event + */ + handleMouseDown (event) { + if (this.isDisabled) return + this.$emit('mousedown', event) + event.preventDefault() + + let newValue = this.getNewValue(event) + if (newValue !== this.actualValue) { + this.updateValue(newValue) + } + + document.body.addEventListener('mousemove', this.handleMouseMove) + document.body.addEventListener('touchmove', this.handleMouseMove) + document.body.addEventListener('mouseup', this.handleMouseUp) + document.body.addEventListener('touchend', this.handleMouseUp) + this.thumbNode && this.thumbNode.focus() + }, + + /** + * Handles slider thumb mousemove event + * @param {Event} event + */ + handleMouseMove (event) { + let newValue = this.getNewValue(event) + this.updateValue(newValue) + } + }, + render (h) { + const children = this.$slots.default || [] + + return h(Box, { + props: { + ...this.$props, + ...this.sliderStyles, + py: 3 + }, + attrs: { + role: 'presentation', + 'aria-disabled': this.isDisabled + }, + style: { + touchAction: 'none' + }, + nativeOn: { + mousedown: this.handleMouseDown, + touchstart: this.handleMouseDown, + mouseleave: this.handleMouseUp, + touchend: this.handleMouseUp, + blur: (event) => { + this.handleMouseUp(event) + this.$emit('blur', event) + } + } + }, [ + ...children, + h('input', { + attrs: { + type: 'hidden', + value: this.actualValue, + name: this.name, + id: this._id + } + }) + ]) + } +} + +/** + * SliderTrack compoennt + */ +const SliderTrack = { + name: 'SliderTrack', + inject: ['$SliderContext', '$theme', '$colorMode'], + props: baseProps, + computed: { + context () { + return this.$SliderContext() + }, + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + }, + trackStyles () { + const { trackStyle } = useSliderStyle({ + ...this.context, + theme: this.theme, + colorMode: this.colorMode + }) + return trackStyle + } + }, + render (h) { + const { isDisabled, trackId } = this.context + return h(Box, { + props: { + ...this.trackStyles, + ...forwardProps(this.$props) + }, + attrs: { + id: trackId, + 'data-slider-track': '', + 'aria-disabled': isDisabled + } + }, this.$slots.default) + } +} + +/** + * SliderFilledTrack component + */ +const SliderFilledTrack = { + name: 'SliderFilledTrack', + inject: ['$SliderContext', '$theme', '$colorMode'], + props: styleProps, + computed: { + context () { + return this.$SliderContext() + }, + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + }, + filledTrackStyles () { + const { filledTrackStyle } = useSliderStyle({ + ...this.context, + theme: this.theme, + colorMode: this.colorMode + }) + return filledTrackStyle + } + }, + render (h) { + const { isDisabled } = this.context + return h(PseudoBox, { + props: { + ...this.filledTrackStyles, + ...forwardProps(this.$props) + }, + attrs: { + 'aria-disabled': isDisabled, + 'data-slider-filled-track': '' + } + }, this.$slots.default) + } +} + +/** + * SliderThumb component + */ +const SliderThumb = { + name: 'SliderThumb', + inject: ['$SliderContext', '$theme', '$colorMode'], + props: baseProps, + computed: { + context () { + return this.$SliderContext() + }, + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + } + }, + render (h) { + const { + thumbId, + isDisabled, + onFocus, + onThumbKeyDown: onKeyDown, + min, + max, + valueText, + orientation, + trackPercent, + size, + color, + value, + ariaLabelledBy + } = this.context + + const { thumbStyle } = useSliderStyle({ + trackPercent, + orientation, + color, + size, + theme: this.theme, + colorMode: this.colorMode + }) + + return h(PseudoBox, { + props: { + d: 'flex', + alignItems: 'center', + outline: 'none', + justifyContent: 'center', + ...thumbStyle, + ...forwardProps(this.$props) + }, + attrs: { + id: thumbId, + role: 'slider', + tabIndex: isDisabled ? undefined : 0, + 'aria-disabled': isDisabled, + 'aria-valuemin': min, + 'aria-valuetext': valueText, + 'aria-orientation': orientation, + 'aria-valuenow': value, + 'aria-valuemax': max, + 'aria-labelledby': ariaLabelledBy + }, + nativeOn: { + keydown: onKeyDown, + focus: onFocus + } + }, this.$slots.default) + } +} + +export default Slider +export { + SliderTrack, + SliderFilledTrack, + SliderThumb +} diff --git a/packages/chakra-ui-core/src/Slider/index.js b/packages/chakra-ui-core/src/Slider/index.js index 6268cbf4..a5023e2e 100644 --- a/packages/chakra-ui-core/src/Slider/index.js +++ b/packages/chakra-ui-core/src/Slider/index.js @@ -1,458 +1,3 @@ -import { baseProps } from '../config' -import { isDef, valueToPercent, useId, getElById, forwardProps } from '../utils' -import { clampValue, roundValueToStep } from './slider.utils' -import Box from '../Box' -import useSliderStyle from './slider.styles' -import { percentToValue } from '../utils/transform' -import styleProps from '../config/props' -import PseudoBox from '../PseudoBox' - -const Slider = { - name: 'Slider', - inject: ['$theme', '$colorMode'], - props: { - ...baseProps, - value: Number, - defaultValue: Number, - isDisabled: Boolean, - max: { - type: Number, - default: 100 - }, - min: { - type: Number, - default: 0 - }, - step: { - type: Number, - default: 1 - }, - ariaLabel: String, - ariaLabelledBy: String, - ariaValueText: String, - orientation: { - type: String, - default: 'horizontal' - }, - getAriaValueText: Function, - size: { - type: String, - default: 'md' - }, - color: { - type: String, - default: 'blue' - }, - name: String, - id: String - }, - provide () { - return { - $SliderContext: () => this.SliderContext - } - }, - data () { - return { - innerValue: this.defaultValue || 0, - trackNode: undefined, - thumbNode: undefined - } - }, - computed: { - isControlled () { - return isDef(this.value) - }, - _value () { - return this.isControlled ? this.value : this.innerValue - }, - actualValue () { - return clampValue(this._value, this.min, this.max) - }, - trackPercentage () { - return valueToPercent(this.actualValue, this.min, this.max) - }, - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - }, - _id () { - return this.id || useId() - }, - trackId () { - return `slider-track-${this._id}` - }, - thumbId () { - return `slider-thumb-${this._id}` - }, - sliderStyles () { - const { rootStyle } = useSliderStyle({ - color: this.color, - colorMode: this.colorMode, - size: this.size, - theme: this.theme, - trackPercent: this.trackPercentage - }) - return rootStyle - }, - valueText () { - return this.getAriaValueText - ? this.getAriaValueText(this.actualValue) - : this.ariaValueText - }, - SliderContext () { - return { - trackNode: this.trackNode, - thumbNode: this.thumbNode, - onThumbKeyDown: this.handleThumbKeyDown, - onFocus: (e) => this.$emit('focus', e), - trackPercent: this.trackPercentage, - ariaLabelledBy: this.ariaLabelledBy, - orientation: this.orientation, - isDisabled: this.isDisabled, - size: this.size, - color: this.color, - min: this.min, - max: this.max, - valueText: this.valueText, - value: this.actualValue, - trackId: this.trackId, - thumbId: this.thumbId - } - } - }, - mounted () { - this.$nextTick(() => { - this.trackNode = getElById(this.trackId) - this.thumbNode = getElById(this.thumbId) - }) - }, - methods: { - /** - * Parses new value returned from slider change event - * @param {Event} event - */ - getNewValue (event) { - if (this.trackNode) { - const { left, width } = this.trackNode.getBoundingClientRect() - const { clientX } = event.touches ? event.touches[0] : event - - let diffX = clientX - left - let percent = diffX / width - let newValue = percentToValue(percent, this.min, this.max) - - if (this.step) { - newValue = roundValueToStep(newValue, this.step) - } - newValue = clampValue(newValue, this.min, this.max) - return newValue - } - }, - - /** - * Updates current inner value - * @param {Number} newValue New Value - */ - updateValue (newValue) { - if (!this.isControlled) { - this.innerValue = newValue - } - this.$emit('change', newValue) - }, - - /** - * Handles SliderThumb keydown event - * @param {Event} event - */ - handleThumbKeyDown (event) { - let flag = false - let newValue - const tenSteps = (this.max - this.min) / 10 - - switch (event.key) { - case 'ArrowLeft': - case 'ArrowDown': - newValue = this.actualValue - this.step - flag = true - break - case 'ArrowRight': - case 'ArrowUp': - newValue = this.actualValue + this.step - flag = true - break - case 'PageDown': - newValue = this.actualValue - tenSteps - flag = true - break - case 'PageUp': - newValue = this.actualValue + tenSteps - flag = true - break - case 'Home': - newValue = this.min - flag = true - break - case 'End': - newValue = this.max - flag = true - break - default: - return - } - - if (flag) { - event.preventDefault() - event.stopPropagation() - } - if (this.step) { - newValue = roundValueToStep(newValue, this.step) - } - newValue = clampValue(newValue, this.min, this.max) - this.updateValue(newValue) - - this.$emit('keydown', event) - }, - - /** - * Handle sliderthumb mouseup event - */ - handleMouseUp () { - document.body.removeEventListener('mousemove', this.handleMouseMove) - document.body.removeEventListener('touchmove', this.handleMouseMove) - document.body.removeEventListener('mouseup', this.handleMouseUp) - document.body.removeEventListener('touchend', this.handleMouseUp) - }, - - /** - * Handles mousedown event for slider - * @param {Event} event - */ - handleMouseDown (event) { - if (this.isDisabled) return - this.$emit('mousedown', event) - event.preventDefault() - - let newValue = this.getNewValue(event) - if (newValue !== this.actualValue) { - this.updateValue(newValue) - } - - document.body.addEventListener('mousemove', this.handleMouseMove) - document.body.addEventListener('touchmove', this.handleMouseMove) - document.body.addEventListener('mouseup', this.handleMouseUp) - document.body.addEventListener('touchend', this.handleMouseUp) - this.thumbNode && this.thumbNode.focus() - }, - - /** - * Handles slider thumb mousemove event - * @param {Event} event - */ - handleMouseMove (event) { - let newValue = this.getNewValue(event) - this.updateValue(newValue) - } - }, - render (h) { - const children = this.$slots.default || [] - - return h(Box, { - props: { - ...this.$props, - ...this.sliderStyles, - py: 3 - }, - attrs: { - role: 'presentation', - 'aria-disabled': this.isDisabled - }, - style: { - touchAction: 'none' - }, - nativeOn: { - mousedown: this.handleMouseDown, - touchstart: this.handleMouseDown, - mouseleave: this.handleMouseUp, - touchend: this.handleMouseUp, - blur: (event) => { - this.handleMouseUp(event) - this.$emit('blur', event) - } - } - }, [ - ...children, - h('input', { - attrs: { - type: 'hidden', - value: this.actualValue, - name: this.name, - id: this._id - } - }) - ]) - } -} - -/** - * SliderTrack compoennt - */ -const SliderTrack = { - name: 'SliderTrack', - inject: ['$SliderContext', '$theme', '$colorMode'], - props: baseProps, - computed: { - context () { - return this.$SliderContext() - }, - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - }, - trackStyles () { - const { trackStyle } = useSliderStyle({ - ...this.context, - theme: this.theme, - colorMode: this.colorMode - }) - return trackStyle - } - }, - render (h) { - const { isDisabled, trackId } = this.context - return h(Box, { - props: { - ...this.trackStyles, - ...forwardProps(this.$props) - }, - attrs: { - id: trackId, - 'data-slider-track': '', - 'aria-disabled': isDisabled - } - }, this.$slots.default) - } -} - -/** - * SliderFilledTrack component - */ -const SliderFilledTrack = { - name: 'SliderFilledTrack', - inject: ['$SliderContext', '$theme', '$colorMode'], - props: styleProps, - computed: { - context () { - return this.$SliderContext() - }, - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - }, - filledTrackStyles () { - const { filledTrackStyle } = useSliderStyle({ - ...this.context, - theme: this.theme, - colorMode: this.colorMode - }) - return filledTrackStyle - } - }, - render (h) { - const { isDisabled } = this.context - return h(PseudoBox, { - props: { - ...this.filledTrackStyles, - ...forwardProps(this.$props) - }, - attrs: { - 'aria-disabled': isDisabled, - 'data-slider-filled-track': '' - } - }, this.$slots.default) - } -} - -/** - * SliderThumb component - */ -const SliderThumb = { - name: 'SliderThumb', - inject: ['$SliderContext', '$theme', '$colorMode'], - props: baseProps, - computed: { - context () { - return this.$SliderContext() - }, - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - } - }, - render (h) { - const { - thumbId, - isDisabled, - onFocus, - onThumbKeyDown: onKeyDown, - min, - max, - valueText, - orientation, - trackPercent, - size, - color, - value, - ariaLabelledBy - } = this.context - - const { thumbStyle } = useSliderStyle({ - trackPercent, - orientation, - color, - size, - theme: this.theme, - colorMode: this.colorMode - }) - - return h(PseudoBox, { - props: { - d: 'flex', - alignItems: 'center', - outline: 'none', - justifyContent: 'center', - ...thumbStyle, - ...forwardProps(this.$props) - }, - attrs: { - id: thumbId, - role: 'slider', - tabIndex: isDisabled ? undefined : 0, - 'aria-disabled': isDisabled, - 'aria-valuemin': min, - 'aria-valuetext': valueText, - 'aria-orientation': orientation, - 'aria-valuenow': value, - 'aria-valuemax': max, - 'aria-labelledby': ariaLabelledBy - }, - nativeOn: { - keydown: onKeyDown, - focus: onFocus - } - }, this.$slots.default) - } -} - +import Slider from './Slider' export default Slider -export { - SliderTrack, - SliderFilledTrack, - SliderThumb -} +export * from './Slider' diff --git a/packages/chakra-ui-core/src/Slider/slider.styles.js b/packages/chakra-ui-core/src/Slider/slider.styles.js index 3745d06b..f36dc294 100644 --- a/packages/chakra-ui-core/src/Slider/slider.styles.js +++ b/packages/chakra-ui-core/src/Slider/slider.styles.js @@ -16,7 +16,9 @@ const thumbStyle = ({ thumbSize, trackPercent, theme }) => { left: `calc(${trackPercent}% - ${thumbSize} / 2)`, border: '1px', borderColor: 'transparent', - transition: 'all 0.3s cubic-bezier(0.25, 0.8, 0.5, 1)', + // TODO: Find another more performant way to implement the slider transitions + // We could use the CSS "will-change: width;" property. So for now, abrupt transitions :D + // transition: 'width 0.3s cubic-bezier(0.25, 0.8, 0.5, 1)' _focus: { shadow: 'outline' }, @@ -35,8 +37,10 @@ const filledTrackStyle = ({ trackHeight, trackPercent, color, colorMode }) => { height: trackHeight, bg: colorMode === 'light' ? `${color}.500` : `${color}.200`, width: `${trackPercent}%`, - rounded: 'sm', - transition: 'width 0.3s cubic-bezier(0.25, 0.8, 0.5, 1)' + rounded: 'sm' + // TODO: Find another more performant way to implement the slider transitions + // We could use the CSS "will-change: width;" property. So for now, abrupt transitions :D + // transition: 'width 0.3s cubic-bezier(0.25, 0.8, 0.5, 1)' } } diff --git a/packages/chakra-ui-core/src/Spinner/Spinner.js b/packages/chakra-ui-core/src/Spinner/Spinner.js new file mode 100644 index 00000000..d29730d9 --- /dev/null +++ b/packages/chakra-ui-core/src/Spinner/Spinner.js @@ -0,0 +1,97 @@ +import { keyframes } from 'emotion' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' +import Box from '../Box' +import VisuallyHidden from '../VisuallyHidden' + +const spin = keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +` + +const sizes = { + xs: { + w: '0.75rem', + h: '0.75rem' + }, + sm: { + w: '1rem', + h: '1rem' + }, + md: { + w: '1.5rem', + h: '1.5rem' + }, + lg: { + w: '2rem', + h: '2rem' + }, + xl: { + w: '3rem', + h: '3rem' + } +} + +const createCustomSize = (size) => { + return { + w: size, + h: size + } +} + +const setSizes = (props) => { + return sizes[props.size] || createCustomSize(props.size) +} + +export default { + name: 'Spinner', + props: { + size: { + type: [String, Array], + default: 'md' + }, + label: { + type: String, + default: 'Loading...' + }, + thickness: { + type: [String, Array], + default: '2px' + }, + speed: { + type: [String, Array], + default: '0.45s' + }, + color: { + type: [String, Array], + default: 'gray.200' + }, + emptyColor: { + type: [String, Array], + default: 'transparent' + }, + forwardRef: Object, + ...baseProps + }, + render (h) { + return h(Box, { + ref: this.forwardRef, + props: { + d: 'inline-block', + borderWidth: this.thickness, + borderBottomColor: this.emptyColor, + borderLeftColor: this.emptyColor, + borderStyle: 'solid', + rounded: 'full', + color: this.color, + animation: `${spin} ${this.speed} linear infinite`, + ...setSizes(this.$props), + ...forwardProps(this.$props) + } + }, this.label && h(VisuallyHidden, {}, this.label)) + } +} diff --git a/packages/chakra-ui-core/src/Spinner/index.js b/packages/chakra-ui-core/src/Spinner/index.js index d29730d9..c8dc995f 100644 --- a/packages/chakra-ui-core/src/Spinner/index.js +++ b/packages/chakra-ui-core/src/Spinner/index.js @@ -1,97 +1,2 @@ -import { keyframes } from 'emotion' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' -import Box from '../Box' -import VisuallyHidden from '../VisuallyHidden' - -const spin = keyframes` - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -` - -const sizes = { - xs: { - w: '0.75rem', - h: '0.75rem' - }, - sm: { - w: '1rem', - h: '1rem' - }, - md: { - w: '1.5rem', - h: '1.5rem' - }, - lg: { - w: '2rem', - h: '2rem' - }, - xl: { - w: '3rem', - h: '3rem' - } -} - -const createCustomSize = (size) => { - return { - w: size, - h: size - } -} - -const setSizes = (props) => { - return sizes[props.size] || createCustomSize(props.size) -} - -export default { - name: 'Spinner', - props: { - size: { - type: [String, Array], - default: 'md' - }, - label: { - type: String, - default: 'Loading...' - }, - thickness: { - type: [String, Array], - default: '2px' - }, - speed: { - type: [String, Array], - default: '0.45s' - }, - color: { - type: [String, Array], - default: 'gray.200' - }, - emptyColor: { - type: [String, Array], - default: 'transparent' - }, - forwardRef: Object, - ...baseProps - }, - render (h) { - return h(Box, { - ref: this.forwardRef, - props: { - d: 'inline-block', - borderWidth: this.thickness, - borderBottomColor: this.emptyColor, - borderLeftColor: this.emptyColor, - borderStyle: 'solid', - rounded: 'full', - color: this.color, - animation: `${spin} ${this.speed} linear infinite`, - ...setSizes(this.$props), - ...forwardProps(this.$props) - } - }, this.label && h(VisuallyHidden, {}, this.label)) - } -} +import Spinner from './Spinner' +export default Spinner diff --git a/packages/chakra-ui-core/src/Stack/Stack.js b/packages/chakra-ui-core/src/Stack/Stack.js new file mode 100644 index 00000000..a32dcc39 --- /dev/null +++ b/packages/chakra-ui-core/src/Stack/Stack.js @@ -0,0 +1,96 @@ +import Flex from '../Flex' +import Box from '../Box' +import { baseProps } from '../config/props' +import { StringArray, SNA } from '../config/props/props.types' +import { forwardProps, cloneVNode } from '../utils' + +/** + * Stack is a layout utility component that makes it easy to stack elements together and apply a space between them. + * It composes the Flex component + */ +const Stack = { + name: 'Stack', + props: { + direction: [String, Array], + isInline: { + type: Boolean, + default: false + }, + isReversed: { + type: Boolean, + default: false + }, + align: StringArray, + justify: StringArray, + spacing: { + type: SNA, + default: 2 + }, + shouldWrapChildren: { + type: Boolean, + default: false + }, + ...baseProps + }, + render (h) { + const _isReversed = this.isReversed || (this.direction && this.direction.endsWith('reverse')) + const _isInline = this.isInline || (this.direction && this.direction.startsWith('row')) + let _direction + + if (_isInline) { + _direction = 'row' + } + + if (_isReversed) { + _direction = this.isInline ? 'row-reverse' : 'column-reverse' + } + + if (this.direction) { + _direction = this.direction + } + + if (!_isInline && !_isReversed && !this.direction) { + _direction = 'column' + } + + const children = this.$slots.default.filter(e => e.tag) + const stackables = children.map((node, index) => { + let isLastChild = children.length === index + 1 + let spacingProps = _isInline + ? { [_isReversed ? 'ml' : 'mr']: isLastChild ? null : this.spacing } + : { [_isReversed ? 'mt' : 'mb']: isLastChild ? null : this.spacing } + const clone = cloneVNode(node, h) + const { propsData } = clone.componentOptions + // If children nodes should wrap, we wrap them inside block with + // display set to inline block. + if (this.shouldWrapChildren) { + return h(Box, { + props: { + d: 'inline-block', + ...spacingProps, + ...forwardProps(this.$props) + } + }, [clone]) + } + + // Otherwise we simply set spacing props to current node. + clone.componentOptions.propsData = { + ...propsData, + ...spacingProps + } + + return clone + }) + + return h(Flex, { + props: { + align: this.align, + justify: this.justify, + direction: _direction, + ...forwardProps(this.$props) + } + }, stackables) + } +} + +export default Stack diff --git a/packages/chakra-ui-core/src/Stack/index.js b/packages/chakra-ui-core/src/Stack/index.js index a32dcc39..a7d799e4 100644 --- a/packages/chakra-ui-core/src/Stack/index.js +++ b/packages/chakra-ui-core/src/Stack/index.js @@ -1,96 +1,2 @@ -import Flex from '../Flex' -import Box from '../Box' -import { baseProps } from '../config/props' -import { StringArray, SNA } from '../config/props/props.types' -import { forwardProps, cloneVNode } from '../utils' - -/** - * Stack is a layout utility component that makes it easy to stack elements together and apply a space between them. - * It composes the Flex component - */ -const Stack = { - name: 'Stack', - props: { - direction: [String, Array], - isInline: { - type: Boolean, - default: false - }, - isReversed: { - type: Boolean, - default: false - }, - align: StringArray, - justify: StringArray, - spacing: { - type: SNA, - default: 2 - }, - shouldWrapChildren: { - type: Boolean, - default: false - }, - ...baseProps - }, - render (h) { - const _isReversed = this.isReversed || (this.direction && this.direction.endsWith('reverse')) - const _isInline = this.isInline || (this.direction && this.direction.startsWith('row')) - let _direction - - if (_isInline) { - _direction = 'row' - } - - if (_isReversed) { - _direction = this.isInline ? 'row-reverse' : 'column-reverse' - } - - if (this.direction) { - _direction = this.direction - } - - if (!_isInline && !_isReversed && !this.direction) { - _direction = 'column' - } - - const children = this.$slots.default.filter(e => e.tag) - const stackables = children.map((node, index) => { - let isLastChild = children.length === index + 1 - let spacingProps = _isInline - ? { [_isReversed ? 'ml' : 'mr']: isLastChild ? null : this.spacing } - : { [_isReversed ? 'mt' : 'mb']: isLastChild ? null : this.spacing } - const clone = cloneVNode(node, h) - const { propsData } = clone.componentOptions - // If children nodes should wrap, we wrap them inside block with - // display set to inline block. - if (this.shouldWrapChildren) { - return h(Box, { - props: { - d: 'inline-block', - ...spacingProps, - ...forwardProps(this.$props) - } - }, [clone]) - } - - // Otherwise we simply set spacing props to current node. - clone.componentOptions.propsData = { - ...propsData, - ...spacingProps - } - - return clone - }) - - return h(Flex, { - props: { - align: this.align, - justify: this.justify, - direction: _direction, - ...forwardProps(this.$props) - } - }, stackables) - } -} - +import Stack from './Stack' export default Stack diff --git a/packages/chakra-ui-core/src/Stat/Stat.js b/packages/chakra-ui-core/src/Stat/Stat.js new file mode 100644 index 00000000..0316d83a --- /dev/null +++ b/packages/chakra-ui-core/src/Stat/Stat.js @@ -0,0 +1,141 @@ +import Box from '../Box' +import Flex from '../Flex' +import { cleanChildren, forwardProps } from '../utils' +import Icon from '../Icon' +import Text from '../Text' + +/** + * Stat Arrow options + */ +const arrowOptions = { + increase: { + name: 'triangle-up', + color: 'green.400' + }, + decrease: { + name: 'triangle-down', + color: 'red.400' + } +} + +/** + * Stat component + */ +const Stat = { + name: 'Stat', + extends: Box, + render (h) { + const children = cleanChildren(this.$slots.default) + return h(Box, { + props: { + flex: 1, + pr: 4, + position: 'relative', + ...forwardProps(this.$props) + } + }, children) + } +} + +/** + * StatGroup component + */ +const StatGroup = { + name: 'StatGroup', + extends: Flex, + render (h) { + const children = cleanChildren(this.$slots.default) + return h(Flex, { + props: { + flexWrap: 'wrap', + justifyContent: 'space-around', + alignItems: 'flex-start', + ...forwardProps(this.$props) + } + }, children) + } +} + +const StatArrow = { + name: 'StatArrow', + extends: Icon, + props: { + type: { + type: String, + default: 'increase' + } + }, + render (h) { + return h(Icon, { + props: { + mr: 1, + size: '14px', + verticalAlign: 'middle', + ...arrowOptions[this.type], + ...forwardProps(this.$props) + } + }) + } +} + +/** + * StatNumber compoennt + */ +const StatNumber = { + name: 'StatNumber', + extends: Text, + render (h) { + return h(Text, { + props: { + fontSize: '2xl', + verticalAlign: 'baseline', + fontWeight: 'semibold', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +/** + * StatHelperText component + */ +const StatHelperText = { + name: 'StatHelperText', + extends: Text, + render (h) { + return h(Text, { + props: { + fontSize: 'sm', + opacity: 0.8, + mb: 2, + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +/** + * StatLabel component + */ +const StatLabel = { + name: 'StatLabel', + extends: Text, + render (h) { + return h(Text, { + props: { + fontWeight: 'medium', + fontSize: 'sm', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +export { + Stat, + StatGroup, + StatArrow, + StatNumber, + StatHelperText, + StatLabel +} diff --git a/packages/chakra-ui-core/src/Stat/index.js b/packages/chakra-ui-core/src/Stat/index.js index 0316d83a..b18bcd8d 100644 --- a/packages/chakra-ui-core/src/Stat/index.js +++ b/packages/chakra-ui-core/src/Stat/index.js @@ -1,141 +1 @@ -import Box from '../Box' -import Flex from '../Flex' -import { cleanChildren, forwardProps } from '../utils' -import Icon from '../Icon' -import Text from '../Text' - -/** - * Stat Arrow options - */ -const arrowOptions = { - increase: { - name: 'triangle-up', - color: 'green.400' - }, - decrease: { - name: 'triangle-down', - color: 'red.400' - } -} - -/** - * Stat component - */ -const Stat = { - name: 'Stat', - extends: Box, - render (h) { - const children = cleanChildren(this.$slots.default) - return h(Box, { - props: { - flex: 1, - pr: 4, - position: 'relative', - ...forwardProps(this.$props) - } - }, children) - } -} - -/** - * StatGroup component - */ -const StatGroup = { - name: 'StatGroup', - extends: Flex, - render (h) { - const children = cleanChildren(this.$slots.default) - return h(Flex, { - props: { - flexWrap: 'wrap', - justifyContent: 'space-around', - alignItems: 'flex-start', - ...forwardProps(this.$props) - } - }, children) - } -} - -const StatArrow = { - name: 'StatArrow', - extends: Icon, - props: { - type: { - type: String, - default: 'increase' - } - }, - render (h) { - return h(Icon, { - props: { - mr: 1, - size: '14px', - verticalAlign: 'middle', - ...arrowOptions[this.type], - ...forwardProps(this.$props) - } - }) - } -} - -/** - * StatNumber compoennt - */ -const StatNumber = { - name: 'StatNumber', - extends: Text, - render (h) { - return h(Text, { - props: { - fontSize: '2xl', - verticalAlign: 'baseline', - fontWeight: 'semibold', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -/** - * StatHelperText component - */ -const StatHelperText = { - name: 'StatHelperText', - extends: Text, - render (h) { - return h(Text, { - props: { - fontSize: 'sm', - opacity: 0.8, - mb: 2, - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -/** - * StatLabel component - */ -const StatLabel = { - name: 'StatLabel', - extends: Text, - render (h) { - return h(Text, { - props: { - fontWeight: 'medium', - fontSize: 'sm', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -export { - Stat, - StatGroup, - StatArrow, - StatNumber, - StatHelperText, - StatLabel -} +export * from './Stat' diff --git a/packages/chakra-ui-core/src/Switch/Switch.js b/packages/chakra-ui-core/src/Switch/Switch.js new file mode 100644 index 00000000..c6a27d80 --- /dev/null +++ b/packages/chakra-ui-core/src/Switch/Switch.js @@ -0,0 +1,139 @@ +import Box from '../Box' +import styleProps from '../config/props' +import { forwardProps } from '../utils' +import VisuallyHidden from '../VisuallyHidden' +import ControlBox from '../ControlBox' + +const switchSizes = { + sm: { + width: '1.375rem', + height: '0.75rem' + }, + md: { + width: '1.875rem', + height: '1rem' + }, + lg: { + width: '2.875rem', + height: '1.5rem' + } +} + +const Switch = { + name: 'CSwitch', + model: { + prop: 'isChecked', + event: 'change' + }, + inject: ['$colorMode'], + props: { + ...styleProps, + id: String, + name: String, + value: Boolean, + _ariaLabel: String, + _ariaLabelledBy: String, + color: { + type: String, + default: 'blue' + }, + defaultIsChecked: Boolean, + isChecked: Boolean, + size: { + type: String, + default: 'md' + }, + isDisabled: Boolean, + isInvalid: Boolean + }, + computed: { + colorMode () { + return this.$colorMode() + }, + _width () { + return switchSizes[this.size] && switchSizes[this.size]['width'] + }, + _height () { + return switchSizes[this.size] && switchSizes[this.size]['height'] + }, + styleProps () { + return { + rounded: 'full', + justifyContent: 'flex-start', + width: this._width, + height: this._height, + bg: this.colorMode === 'dark' ? 'whiteAlpha.400' : 'gray.300', + boxSizing: 'content-box', + p: '2px', + _checked: { + bg: `${this.color}.500` + }, + _child: { + transform: `translateX(0)` + }, + _checkedAndChild: { + transform: `translateX(calc(${this._width} - ${this._height}))` + }, + _focus: { + boxShadow: 'outline' + }, + _hover: { + cursor: 'pointer' + }, + _checkedAndHover: { + cursor: 'pointer' + }, + _disabled: { + opacity: 0.4, + cursor: 'not-allowed' + } + } + } + }, + render (h) { + return h(Box, { + props: { + ...forwardProps(this.$props), + as: 'label', + display: 'inline-block', + verticalAlign: 'middle' + } + }, [ + h(VisuallyHidden, { + props: { + as: 'input' + }, + attrs: { + type: 'checkbox', + 'aria-label': this._ariaLabel, + 'aria-labelledby': this._ariaLabelledBy, + id: this.id, + name: this.name, + value: this.value, + 'aria-invalid': this.isInvalid, + defaultChecked: this.defaultIsChecked, + checked: this.isChecked, + disabled: this.isDisabled + }, + nativeOn: { + change: (e) => this.$emit('change', !this.isChecked, e) + } + }), + h(ControlBox, { + props: this.styleProps + }, [ + h(Box, { + props: { + bg: 'white', + transition: 'transform 250ms', + rounded: 'full', + h: this._height, + w: this._height + } + }) + ]) + ]) + } +} + +export default Switch diff --git a/packages/chakra-ui-core/src/Switch/index.js b/packages/chakra-ui-core/src/Switch/index.js index c6a27d80..7afa7580 100644 --- a/packages/chakra-ui-core/src/Switch/index.js +++ b/packages/chakra-ui-core/src/Switch/index.js @@ -1,139 +1,2 @@ -import Box from '../Box' -import styleProps from '../config/props' -import { forwardProps } from '../utils' -import VisuallyHidden from '../VisuallyHidden' -import ControlBox from '../ControlBox' - -const switchSizes = { - sm: { - width: '1.375rem', - height: '0.75rem' - }, - md: { - width: '1.875rem', - height: '1rem' - }, - lg: { - width: '2.875rem', - height: '1.5rem' - } -} - -const Switch = { - name: 'CSwitch', - model: { - prop: 'isChecked', - event: 'change' - }, - inject: ['$colorMode'], - props: { - ...styleProps, - id: String, - name: String, - value: Boolean, - _ariaLabel: String, - _ariaLabelledBy: String, - color: { - type: String, - default: 'blue' - }, - defaultIsChecked: Boolean, - isChecked: Boolean, - size: { - type: String, - default: 'md' - }, - isDisabled: Boolean, - isInvalid: Boolean - }, - computed: { - colorMode () { - return this.$colorMode() - }, - _width () { - return switchSizes[this.size] && switchSizes[this.size]['width'] - }, - _height () { - return switchSizes[this.size] && switchSizes[this.size]['height'] - }, - styleProps () { - return { - rounded: 'full', - justifyContent: 'flex-start', - width: this._width, - height: this._height, - bg: this.colorMode === 'dark' ? 'whiteAlpha.400' : 'gray.300', - boxSizing: 'content-box', - p: '2px', - _checked: { - bg: `${this.color}.500` - }, - _child: { - transform: `translateX(0)` - }, - _checkedAndChild: { - transform: `translateX(calc(${this._width} - ${this._height}))` - }, - _focus: { - boxShadow: 'outline' - }, - _hover: { - cursor: 'pointer' - }, - _checkedAndHover: { - cursor: 'pointer' - }, - _disabled: { - opacity: 0.4, - cursor: 'not-allowed' - } - } - } - }, - render (h) { - return h(Box, { - props: { - ...forwardProps(this.$props), - as: 'label', - display: 'inline-block', - verticalAlign: 'middle' - } - }, [ - h(VisuallyHidden, { - props: { - as: 'input' - }, - attrs: { - type: 'checkbox', - 'aria-label': this._ariaLabel, - 'aria-labelledby': this._ariaLabelledBy, - id: this.id, - name: this.name, - value: this.value, - 'aria-invalid': this.isInvalid, - defaultChecked: this.defaultIsChecked, - checked: this.isChecked, - disabled: this.isDisabled - }, - nativeOn: { - change: (e) => this.$emit('change', !this.isChecked, e) - } - }), - h(ControlBox, { - props: this.styleProps - }, [ - h(Box, { - props: { - bg: 'white', - transition: 'transform 250ms', - rounded: 'full', - h: this._height, - w: this._height - } - }) - ]) - ]) - } -} - +import Switch from './Switch' export default Switch diff --git a/packages/chakra-ui-core/src/Tabs/Tabs.js b/packages/chakra-ui-core/src/Tabs/Tabs.js new file mode 100644 index 00000000..d324464f --- /dev/null +++ b/packages/chakra-ui-core/src/Tabs/Tabs.js @@ -0,0 +1,440 @@ +import { baseProps } from '../config' +import { useVariantColorWarning, isDef, useId, forwardProps, cleanChildren, cloneVNodeElement } from '../utils' +import { useTabListStyle, useTabStyle } from './tabs.styles' +import styleProps from '../config/props' +import Flex from '../Flex' +import Box from '../Box' +import PseudoBox from '../PseudoBox' + +const Tabs = { + name: 'Tabs', + inject: ['$theme', '$colorMode'], + props: { + ...baseProps, + index: Number, + defaultIndex: Number, + isManual: Boolean, + variant: { + type: String, + default: 'line' + }, + variantColor: { + type: String, + default: 'blue' + }, + align: { + type: String, + default: 'start' + }, + size: { + type: String, + default: 'md' + }, + orientation: { + type: String, + default: 'horizontal' + }, + isFitted: Boolean + }, + provide () { + return { + $TabContext: () => this.TabContext + } + }, + data () { + return { + selectedPanelNode: undefined, + selectedIndex: this.getInitialIndex(), + manualIndex: this.index || this.defaultIndex || 0 + } + }, + computed: { + TabContext () { + return { + id: this.id, + selectedIndex: this.selectedIndex, + index: this.actualIdx, + manualIndex: this.manualIdx, + onManualTabChange: this.onManualTabChange, + isManual: this.isManual, + onChangeTab: this.onChangeTab, + selectedPanelRef: this.selectedPanelRef, + onFocusPanel: this.onFocusPanel, + color: this.variantColor, + size: this.size, + align: this.align, + variant: this.variant, + isFitted: this.isFitted, + orientation: this.orientation, + set: this.set + } + }, + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + }, + isControlled () { + return isDef(this.index) + }, + id () { + return `tabs-${useId()}` + }, + actualIdx () { + if (!this.isManual) { + return this.defaultIndex || 0 + } else { + return this.index || this.defaultIndex || 0 + } + }, + manualIdx () { + return this.isControlled ? this.index : this.manualIndex + } + }, + created () { + useVariantColorWarning(this.theme, 'Tabs', this.variantColor) + }, + methods: { + /** + * Gets initial active tab index + */ + getInitialIndex () { + if (!this.isManual) { + return this.defaultIndex || 0 + } else { + return this.index || this.defaultIndex || 0 + } + }, + + /** + * Handles tab chage + * @param {Number} index Index to vbe set + */ + onChangeTab (index) { + if (!this.isControlled) { + this.selectedIndex = index + } + + if (this.isControlled && this.isManual) { + this.selectedIndex = index + } + + if (!this.isManual) { + this.$emit('change', index) + } + }, + + /** + * Manual tab change handler + * @param {Number} index Index of tab to set + */ + onManualTabChange (index) { + if (!this.isControlled) { + this.manualIndex = index + } + + if (this.isManual) { + this.$emit('change', index) + } + }, + + /** + * Focuses on active tab + */ + onFocusPanel () { + if (this.selectedPanelNode) { + this.selectedPanelNode.focus() + } + }, + + /** + * Sets the value of any component instance property. + * This function is to be passed down to context so that consumers + * can mutate context values with out doing it directly. + * Serves as a temporary fix until Vue 3 comes out + * @param {String} prop Component instance property + * @param {Any} value Property value + */ + set (prop, value) { + this[prop] = value + return this[prop] + } + }, + render (h) { + return h(Box, { + props: forwardProps(this.$props) + }, this.$slots.default) + } +} + +const TabList = { + name: 'TabList', + props: baseProps, + inject: ['$TabContext', '$theme', '$colorMode'], + data () { + return { + allNodes: {}, + validChildren: [], + focusableIndexes: [] + } + }, + computed: { + context () { + return this.$TabContext() + }, + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + }, + tabListStyleProps () { + const { align, variant, orientation } = this.context + return useTabListStyle({ + theme: this.theme, + align, + orientation, + variant + }) + }, + enabledSelectedIndex () { + const { selectedIndex } = this.context + return this.focusableIndexes.indexOf(selectedIndex) + }, + count () { + return this.focusableIndexes.length + } + }, + mounted () { + this.$nextTick(() => { + const children = this.$el.children + this.allNodes = Object.assign({}, children) + }) + }, + methods: { + + /** + * Updates current Index + * @param {Number} index Index + */ + updateIndex (index) { + const { onChangeTab } = this.context + const childIndex = this.focusableIndexes[index] + this.allNodes[childIndex].focus() + onChangeTab && onChangeTab(childIndex) + }, + + /** + * Handles keydown event + * @param {Event} event event + */ + handleKeyDown (event) { + const { onFocusPanel } = this.context + + if (event.key === 'ArrowRight') { + event.preventDefault() + const nextIndex = (this.enabledSelectedIndex + 1) % this.count + this.updateIndex(nextIndex) + } + + if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { + event.preventDefault() + const nextIndex = (this.enabledSelectedIndex - 1 + this.count) % this.count + this.updateIndex(nextIndex) + } + + if (event.key === 'Home') { + event.preventDefault() + this.updateIndex(0) + } + + if (event.key === 'End') { + event.preventDefault() + this.updateIndex(this.count - 1) + } + + if (event.key === 'ArrowDown') { + event.preventDefault() + onFocusPanel && onFocusPanel() + } + + this.$emit('keydown', event) + } + }, + render (h) { + this.validChildren = cleanChildren(this.$slots.default) + + const { id, isManual, manualIndex, selectedIndex, onManualTabChange, onChangeTab, orientation } = this.context + const validChildren = cleanChildren(this.$slots.default) + const clones = validChildren.map((vnode, index) => { + let isSelected = isManual ? index === manualIndex : index === selectedIndex + + const handleClick = event => { + // Hack for Safari. Buttons don't receive focus on click on Safari + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus + this.allNodes[index].focus() + + onManualTabChange(index) + onChangeTab(index) + + this.$emit('click', event) + } + + const clone = cloneVNodeElement(vnode, { + props: { + isSelected + }, + nativeOn: { + click: handleClick + }, + attrs: { + id: `${id}-${index}` + } + }, h) + + return clone + }) + + this.focusableIndexes = clones + .map((child, index) => (child.componentOptions.propsData.isDisabled === true ? null : index)) + .filter(index => index != null) + + return h(Flex, { + attrs: { + role: 'tablist', + 'aria-orientation': orientation + }, + props: { + ...this.tabListStyleProps, + ...forwardProps() + }, + nativeOn: { + keydown: this.handleKeyDown + } + }, clones) + } +} + +const Tab = { + name: 'Tab', + inject: ['$theme', '$colorMode', '$TabContext'], + props: { + ...styleProps, + isSelected: Boolean, + isDisabled: Boolean, + id: String + }, + computed: { + context () { + return this.$TabContext() + }, + tabStyleProps () { + const { color, isFitted, orientation, size, variant } = this.context + const styles = useTabStyle({ + colorMode: this.colorMode, + theme: this.theme, + color, + isFitted, + orientation, + size, + variant + }) + return styles + }, + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + } + }, + render (h) { + return h(PseudoBox, { + props: { + outline: 'none', + as: 'button', + ...this.tabStyleProps, + ...forwardProps(this.$props) + }, + attrs: { + role: 'tab', + tabIndex: this.isSelected ? 0 : -1, + id: `tab:${this.id}`, + type: 'button', + disabled: this.isDisabled, + 'aria-disabled': this.isDisabled, + 'aria-selected': this.isSelected, + 'aria-controls': `panel:${this.id}` + } + }, this.$slots.default) + } +} + +const TabPanel = { + name: 'TabPanel', + props: { + ...baseProps, + isSelected: Boolean, + selectedPanelNode: Object, + id: String + }, + render (h) { + return h(Box, { + attrs: { + role: 'tabpanel', + tabIndex: -1, + 'aria-labelledby': `tab:${this.id}`, + hidden: !this.isSelected, + id: `panel:${this.id}`, + outline: 0, + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} + +const TabPanels = { + name: 'TabPanels', + inject: ['$TabContext'], + props: baseProps, + computed: { + context () { + return this.$TabContext() + } + }, + render (h) { + const { + selectedIndex, + id, + isManual, + manualIndex + } = this.context + + const validChildren = cleanChildren(this.$slots.default) + + const clones = validChildren.map((child, index) => { + return cloneVNodeElement(child, { + props: { + isSelected: isManual ? index === manualIndex : index === selectedIndex, + id: `${id}-${index}` + } + }, h) + }) + + return h(Box, { + props: forwardProps(this.$props), + attrs: { + tabIndex: -1 + } + }, clones) + } +} + +export { + Tabs, + TabList, + Tab, + TabPanels, + TabPanel +} diff --git a/packages/chakra-ui-core/src/Tabs/index.js b/packages/chakra-ui-core/src/Tabs/index.js index d324464f..443079cd 100644 --- a/packages/chakra-ui-core/src/Tabs/index.js +++ b/packages/chakra-ui-core/src/Tabs/index.js @@ -1,440 +1 @@ -import { baseProps } from '../config' -import { useVariantColorWarning, isDef, useId, forwardProps, cleanChildren, cloneVNodeElement } from '../utils' -import { useTabListStyle, useTabStyle } from './tabs.styles' -import styleProps from '../config/props' -import Flex from '../Flex' -import Box from '../Box' -import PseudoBox from '../PseudoBox' - -const Tabs = { - name: 'Tabs', - inject: ['$theme', '$colorMode'], - props: { - ...baseProps, - index: Number, - defaultIndex: Number, - isManual: Boolean, - variant: { - type: String, - default: 'line' - }, - variantColor: { - type: String, - default: 'blue' - }, - align: { - type: String, - default: 'start' - }, - size: { - type: String, - default: 'md' - }, - orientation: { - type: String, - default: 'horizontal' - }, - isFitted: Boolean - }, - provide () { - return { - $TabContext: () => this.TabContext - } - }, - data () { - return { - selectedPanelNode: undefined, - selectedIndex: this.getInitialIndex(), - manualIndex: this.index || this.defaultIndex || 0 - } - }, - computed: { - TabContext () { - return { - id: this.id, - selectedIndex: this.selectedIndex, - index: this.actualIdx, - manualIndex: this.manualIdx, - onManualTabChange: this.onManualTabChange, - isManual: this.isManual, - onChangeTab: this.onChangeTab, - selectedPanelRef: this.selectedPanelRef, - onFocusPanel: this.onFocusPanel, - color: this.variantColor, - size: this.size, - align: this.align, - variant: this.variant, - isFitted: this.isFitted, - orientation: this.orientation, - set: this.set - } - }, - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - }, - isControlled () { - return isDef(this.index) - }, - id () { - return `tabs-${useId()}` - }, - actualIdx () { - if (!this.isManual) { - return this.defaultIndex || 0 - } else { - return this.index || this.defaultIndex || 0 - } - }, - manualIdx () { - return this.isControlled ? this.index : this.manualIndex - } - }, - created () { - useVariantColorWarning(this.theme, 'Tabs', this.variantColor) - }, - methods: { - /** - * Gets initial active tab index - */ - getInitialIndex () { - if (!this.isManual) { - return this.defaultIndex || 0 - } else { - return this.index || this.defaultIndex || 0 - } - }, - - /** - * Handles tab chage - * @param {Number} index Index to vbe set - */ - onChangeTab (index) { - if (!this.isControlled) { - this.selectedIndex = index - } - - if (this.isControlled && this.isManual) { - this.selectedIndex = index - } - - if (!this.isManual) { - this.$emit('change', index) - } - }, - - /** - * Manual tab change handler - * @param {Number} index Index of tab to set - */ - onManualTabChange (index) { - if (!this.isControlled) { - this.manualIndex = index - } - - if (this.isManual) { - this.$emit('change', index) - } - }, - - /** - * Focuses on active tab - */ - onFocusPanel () { - if (this.selectedPanelNode) { - this.selectedPanelNode.focus() - } - }, - - /** - * Sets the value of any component instance property. - * This function is to be passed down to context so that consumers - * can mutate context values with out doing it directly. - * Serves as a temporary fix until Vue 3 comes out - * @param {String} prop Component instance property - * @param {Any} value Property value - */ - set (prop, value) { - this[prop] = value - return this[prop] - } - }, - render (h) { - return h(Box, { - props: forwardProps(this.$props) - }, this.$slots.default) - } -} - -const TabList = { - name: 'TabList', - props: baseProps, - inject: ['$TabContext', '$theme', '$colorMode'], - data () { - return { - allNodes: {}, - validChildren: [], - focusableIndexes: [] - } - }, - computed: { - context () { - return this.$TabContext() - }, - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - }, - tabListStyleProps () { - const { align, variant, orientation } = this.context - return useTabListStyle({ - theme: this.theme, - align, - orientation, - variant - }) - }, - enabledSelectedIndex () { - const { selectedIndex } = this.context - return this.focusableIndexes.indexOf(selectedIndex) - }, - count () { - return this.focusableIndexes.length - } - }, - mounted () { - this.$nextTick(() => { - const children = this.$el.children - this.allNodes = Object.assign({}, children) - }) - }, - methods: { - - /** - * Updates current Index - * @param {Number} index Index - */ - updateIndex (index) { - const { onChangeTab } = this.context - const childIndex = this.focusableIndexes[index] - this.allNodes[childIndex].focus() - onChangeTab && onChangeTab(childIndex) - }, - - /** - * Handles keydown event - * @param {Event} event event - */ - handleKeyDown (event) { - const { onFocusPanel } = this.context - - if (event.key === 'ArrowRight') { - event.preventDefault() - const nextIndex = (this.enabledSelectedIndex + 1) % this.count - this.updateIndex(nextIndex) - } - - if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') { - event.preventDefault() - const nextIndex = (this.enabledSelectedIndex - 1 + this.count) % this.count - this.updateIndex(nextIndex) - } - - if (event.key === 'Home') { - event.preventDefault() - this.updateIndex(0) - } - - if (event.key === 'End') { - event.preventDefault() - this.updateIndex(this.count - 1) - } - - if (event.key === 'ArrowDown') { - event.preventDefault() - onFocusPanel && onFocusPanel() - } - - this.$emit('keydown', event) - } - }, - render (h) { - this.validChildren = cleanChildren(this.$slots.default) - - const { id, isManual, manualIndex, selectedIndex, onManualTabChange, onChangeTab, orientation } = this.context - const validChildren = cleanChildren(this.$slots.default) - const clones = validChildren.map((vnode, index) => { - let isSelected = isManual ? index === manualIndex : index === selectedIndex - - const handleClick = event => { - // Hack for Safari. Buttons don't receive focus on click on Safari - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus - this.allNodes[index].focus() - - onManualTabChange(index) - onChangeTab(index) - - this.$emit('click', event) - } - - const clone = cloneVNodeElement(vnode, { - props: { - isSelected - }, - nativeOn: { - click: handleClick - }, - attrs: { - id: `${id}-${index}` - } - }, h) - - return clone - }) - - this.focusableIndexes = clones - .map((child, index) => (child.componentOptions.propsData.isDisabled === true ? null : index)) - .filter(index => index != null) - - return h(Flex, { - attrs: { - role: 'tablist', - 'aria-orientation': orientation - }, - props: { - ...this.tabListStyleProps, - ...forwardProps() - }, - nativeOn: { - keydown: this.handleKeyDown - } - }, clones) - } -} - -const Tab = { - name: 'Tab', - inject: ['$theme', '$colorMode', '$TabContext'], - props: { - ...styleProps, - isSelected: Boolean, - isDisabled: Boolean, - id: String - }, - computed: { - context () { - return this.$TabContext() - }, - tabStyleProps () { - const { color, isFitted, orientation, size, variant } = this.context - const styles = useTabStyle({ - colorMode: this.colorMode, - theme: this.theme, - color, - isFitted, - orientation, - size, - variant - }) - return styles - }, - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - } - }, - render (h) { - return h(PseudoBox, { - props: { - outline: 'none', - as: 'button', - ...this.tabStyleProps, - ...forwardProps(this.$props) - }, - attrs: { - role: 'tab', - tabIndex: this.isSelected ? 0 : -1, - id: `tab:${this.id}`, - type: 'button', - disabled: this.isDisabled, - 'aria-disabled': this.isDisabled, - 'aria-selected': this.isSelected, - 'aria-controls': `panel:${this.id}` - } - }, this.$slots.default) - } -} - -const TabPanel = { - name: 'TabPanel', - props: { - ...baseProps, - isSelected: Boolean, - selectedPanelNode: Object, - id: String - }, - render (h) { - return h(Box, { - attrs: { - role: 'tabpanel', - tabIndex: -1, - 'aria-labelledby': `tab:${this.id}`, - hidden: !this.isSelected, - id: `panel:${this.id}`, - outline: 0, - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} - -const TabPanels = { - name: 'TabPanels', - inject: ['$TabContext'], - props: baseProps, - computed: { - context () { - return this.$TabContext() - } - }, - render (h) { - const { - selectedIndex, - id, - isManual, - manualIndex - } = this.context - - const validChildren = cleanChildren(this.$slots.default) - - const clones = validChildren.map((child, index) => { - return cloneVNodeElement(child, { - props: { - isSelected: isManual ? index === manualIndex : index === selectedIndex, - id: `${id}-${index}` - } - }, h) - }) - - return h(Box, { - props: forwardProps(this.$props), - attrs: { - tabIndex: -1 - } - }, clones) - } -} - -export { - Tabs, - TabList, - Tab, - TabPanels, - TabPanel -} +export * from './Tabs' diff --git a/packages/chakra-ui-core/src/Tag/Tag.js b/packages/chakra-ui-core/src/Tag/Tag.js new file mode 100644 index 00000000..fe53331e --- /dev/null +++ b/packages/chakra-ui-core/src/Tag/Tag.js @@ -0,0 +1,191 @@ +import styleProps, { baseProps } from '../config/props' +import { css } from 'emotion' +import PseudoBox from '../PseudoBox' +import Icon from '../Icon' +import Box from '../Box' +import { useVariantColorWarning, forwardProps } from '../utils' +import useBadgeStyle from '../Badge/badge.styles' + +const tagSizes = { + sm: { + minH: 6, + minW: 6, + fontSize: 'sm', + px: 2 + }, + md: { + minH: '1.75rem', + minW: '1.75rem', + fontSize: 'sm', + px: 2 + }, + lg: { + minH: 8, + minW: 8, + px: 3 + } +} + +const TagCloseButton = { + name: 'TagCloseButton', + props: { + ...styleProps, + isDisabled: Boolean + }, + render (h) { + return h(PseudoBox, { + props: { + ...this.$props, + as: 'button', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'all 0.2s', + rounded: 'full', + size: '1.25rem', + outline: 'none', + opacity: '0.5', + mr: -1, + _disabled: { + opacity: '40%', + cursor: 'not-allowed', + boxShadow: 'none' + }, + _focus: { + boxShadow: 'outline', + bg: 'rgba(0, 0, 0, 0.14)' + }, + _hover: { + opacity: '0.8' + }, + _active: { + opacity: '1' + } + }, + attrs: { + disabled: this.isDisabled + } + }, [ + h(Icon, { + props: { + size: '18px', + name: 'small-close' + }, + attrs: { + focusable: false + } + }) + ]) + } +} + +const TagIcon = { + name: 'TagIcon', + props: { + ...baseProps, + icon: [String, Object] + }, + render (h) { + if (typeof this.icon === 'string') { + return h(Icon, { + class: [css({ + '&:first-child': { marginLeft: 0 }, + '&:last-child': { marginRight: 0 } + })], + props: { + ...this.$props, + name: this.icon, + mx: '0.5rem' + } + }) + } + + return h(Box, { + class: [css({ + '&:first-child': { marginLeft: 0 }, + '&:last-child': { marginRight: 0 } + })], + props: { + ...this.$props, + as: this.icon, + mx: '0.5rem', + color: 'currentColor' + } + }) + } +} + +const TagLabel = { + name: 'TagLabel', + props: baseProps, + render (h) { + return h(Box, { + ...forwardProps(this.$props), + as: 'span', + isTruncated: true, + lineHeight: 1.2 + }, this.$slots.default) + } +} + +const Tag = { + name: 'Tag', + inject: ['$theme', '$colorMode'], + props: { + ...styleProps, + variant: { + type: String, + default: 'subtle' + }, + size: { + type: String, + default: 'lg' + }, + variantColor: { + type: String, + default: 'gray' + } + }, + computed: { + theme () { + return this.$theme() + }, + colorMode () { + return this.$colorMode() + }, + styleProps () { + useVariantColorWarning(this.theme, 'Tag', this.variantColor) + return useBadgeStyle({ + variant: this.variant, + color: this.variantColor, + colorMode: this.colorMode, + theme: this.theme + }) + }, + sizeProps () { + return tagSizes[this.size] + } + }, + render (h) { + return h(PseudoBox, { + props: { + display: 'inline-flex', + alignItems: 'center', + minH: 6, + maxW: '100%', + rounded: 'md', + fontWeight: 'medium', + ...forwardProps(this.$props), + ...this.sizeProps, + ...this.styleProps + } + }, this.$slots.default) + } +} + +export { + Tag, + TagLabel, + TagIcon, + TagCloseButton +} diff --git a/packages/chakra-ui-core/src/Tag/index.js b/packages/chakra-ui-core/src/Tag/index.js index fe53331e..fd1a1e7e 100644 --- a/packages/chakra-ui-core/src/Tag/index.js +++ b/packages/chakra-ui-core/src/Tag/index.js @@ -1,191 +1 @@ -import styleProps, { baseProps } from '../config/props' -import { css } from 'emotion' -import PseudoBox from '../PseudoBox' -import Icon from '../Icon' -import Box from '../Box' -import { useVariantColorWarning, forwardProps } from '../utils' -import useBadgeStyle from '../Badge/badge.styles' - -const tagSizes = { - sm: { - minH: 6, - minW: 6, - fontSize: 'sm', - px: 2 - }, - md: { - minH: '1.75rem', - minW: '1.75rem', - fontSize: 'sm', - px: 2 - }, - lg: { - minH: 8, - minW: 8, - px: 3 - } -} - -const TagCloseButton = { - name: 'TagCloseButton', - props: { - ...styleProps, - isDisabled: Boolean - }, - render (h) { - return h(PseudoBox, { - props: { - ...this.$props, - as: 'button', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - transition: 'all 0.2s', - rounded: 'full', - size: '1.25rem', - outline: 'none', - opacity: '0.5', - mr: -1, - _disabled: { - opacity: '40%', - cursor: 'not-allowed', - boxShadow: 'none' - }, - _focus: { - boxShadow: 'outline', - bg: 'rgba(0, 0, 0, 0.14)' - }, - _hover: { - opacity: '0.8' - }, - _active: { - opacity: '1' - } - }, - attrs: { - disabled: this.isDisabled - } - }, [ - h(Icon, { - props: { - size: '18px', - name: 'small-close' - }, - attrs: { - focusable: false - } - }) - ]) - } -} - -const TagIcon = { - name: 'TagIcon', - props: { - ...baseProps, - icon: [String, Object] - }, - render (h) { - if (typeof this.icon === 'string') { - return h(Icon, { - class: [css({ - '&:first-child': { marginLeft: 0 }, - '&:last-child': { marginRight: 0 } - })], - props: { - ...this.$props, - name: this.icon, - mx: '0.5rem' - } - }) - } - - return h(Box, { - class: [css({ - '&:first-child': { marginLeft: 0 }, - '&:last-child': { marginRight: 0 } - })], - props: { - ...this.$props, - as: this.icon, - mx: '0.5rem', - color: 'currentColor' - } - }) - } -} - -const TagLabel = { - name: 'TagLabel', - props: baseProps, - render (h) { - return h(Box, { - ...forwardProps(this.$props), - as: 'span', - isTruncated: true, - lineHeight: 1.2 - }, this.$slots.default) - } -} - -const Tag = { - name: 'Tag', - inject: ['$theme', '$colorMode'], - props: { - ...styleProps, - variant: { - type: String, - default: 'subtle' - }, - size: { - type: String, - default: 'lg' - }, - variantColor: { - type: String, - default: 'gray' - } - }, - computed: { - theme () { - return this.$theme() - }, - colorMode () { - return this.$colorMode() - }, - styleProps () { - useVariantColorWarning(this.theme, 'Tag', this.variantColor) - return useBadgeStyle({ - variant: this.variant, - color: this.variantColor, - colorMode: this.colorMode, - theme: this.theme - }) - }, - sizeProps () { - return tagSizes[this.size] - } - }, - render (h) { - return h(PseudoBox, { - props: { - display: 'inline-flex', - alignItems: 'center', - minH: 6, - maxW: '100%', - rounded: 'md', - fontWeight: 'medium', - ...forwardProps(this.$props), - ...this.sizeProps, - ...this.styleProps - } - }, this.$slots.default) - } -} - -export { - Tag, - TagLabel, - TagIcon, - TagCloseButton -} +export * from './Tag' diff --git a/packages/chakra-ui-core/src/Text/Text.js b/packages/chakra-ui-core/src/Text/Text.js new file mode 100644 index 00000000..d2f3f5c6 --- /dev/null +++ b/packages/chakra-ui-core/src/Text/Text.js @@ -0,0 +1,24 @@ +import Box from '../Box' +import { forwardProps } from '../utils' +import { baseProps } from '../config/props' + +export default { + name: 'CText', // <-- Vue does not allow components to be registered using built-in or reserved HTML elements as component id: like "Text". So need to rename this. + inject: ['$theme', '$colorMode'], + props: { + as: { + type: [String, Array], + default: 'p' + }, + ...baseProps + }, + render (h) { + return h(Box, { + props: { + as: this.as, + fontFamily: this.as === 'kbd' ? 'mono' : 'body', + ...forwardProps(this.$props) + } + }, this.$slots.default) + } +} diff --git a/packages/chakra-ui-core/src/Text/index.js b/packages/chakra-ui-core/src/Text/index.js index 3d17176e..5a5c68ba 100644 --- a/packages/chakra-ui-core/src/Text/index.js +++ b/packages/chakra-ui-core/src/Text/index.js @@ -1,24 +1,2 @@ -import Box from '../Box' -import { forwardProps } from '../utils' -import { baseProps } from '../config/props' - -export default { - name: 'KText', // <-- Vue does not allow components to be registered using built-in or reserved HTML elements as component id: like "Text". So need to rename this. - inject: ['$theme', '$colorMode'], - props: { - as: { - type: [String, Array], - default: 'p' - }, - ...baseProps - }, - render (h) { - return h(Box, { - props: { - as: this.as, - fontFamily: this.as === 'kbd' ? 'mono' : 'body', - ...forwardProps(this.$props) - } - }, this.$slots.default) - } -} +import Text from './Text' +export default Text diff --git a/packages/chakra-ui-core/src/Textarea/Textarea.js b/packages/chakra-ui-core/src/Textarea/Textarea.js new file mode 100644 index 00000000..21d5d168 --- /dev/null +++ b/packages/chakra-ui-core/src/Textarea/Textarea.js @@ -0,0 +1,34 @@ +import styleProps from '../config/props' +import { inputProps } from '../Input/input.props' +import Input from '../Input' +import { forwardProps } from '../utils' + +const Textarea = { + name: 'Textarea', + model: { + prop: 'inputValue', + event: 'change' + }, + props: { + ...styleProps, + ...inputProps, + inputValue: String + }, + render (h) { + return h(Input, { + props: { + ...forwardProps(this.$props), + as: 'textarea', + py: '8px', + minHeight: '80px', + fontFamily: 'body', + lineHeight: 'shorter' + }, + on: { + input: (value, $e) => this.$emit('change', value, $e) + } + }, this.$slots.default) + } +} + +export default Textarea diff --git a/packages/chakra-ui-core/src/Textarea/index.js b/packages/chakra-ui-core/src/Textarea/index.js index 21d5d168..24f17c99 100644 --- a/packages/chakra-ui-core/src/Textarea/index.js +++ b/packages/chakra-ui-core/src/Textarea/index.js @@ -1,34 +1,2 @@ -import styleProps from '../config/props' -import { inputProps } from '../Input/input.props' -import Input from '../Input' -import { forwardProps } from '../utils' - -const Textarea = { - name: 'Textarea', - model: { - prop: 'inputValue', - event: 'change' - }, - props: { - ...styleProps, - ...inputProps, - inputValue: String - }, - render (h) { - return h(Input, { - props: { - ...forwardProps(this.$props), - as: 'textarea', - py: '8px', - minHeight: '80px', - fontFamily: 'body', - lineHeight: 'shorter' - }, - on: { - input: (value, $e) => this.$emit('change', value, $e) - } - }, this.$slots.default) - } -} - +import Textarea from './Textarea' export default Textarea diff --git a/packages/chakra-ui-core/src/ThemeProvider/ThemeProvider.js b/packages/chakra-ui-core/src/ThemeProvider/ThemeProvider.js new file mode 100644 index 00000000..cb6619c3 --- /dev/null +++ b/packages/chakra-ui-core/src/ThemeProvider/ThemeProvider.js @@ -0,0 +1,48 @@ +import { colorModeObserver } from '../utils/color-mode-observer' + +const ThemeProvider = { + name: 'ThemeProvider', + provide () { + return { + $theme: () => this.theme, + $icons: this.icons, + /** + * By default the ThemeProvider exposes a colorMode value of light + * If no `ColorModeProvider` is provided in children/ consumer app, all chakra + * components will consume the $colorMode from here. + */ + $colorMode: () => 'light' + } + }, + computed: { + icons () { + return this.$chakra ? this.$chakra.icons : {} + }, + theme () { + return this.$chakra.theme + } + }, + watch: { + theme: { + immediate: true, + handler (newVal) { + colorModeObserver.theme = newVal + } + }, + icons: { + immediate: true, + handler (newVal) { + colorModeObserver.icons = newVal + } + } + }, + render (h) { + return h('div', { + attrs: { + id: '__chakra-app' + } + }, this.$slots.default) + } +} + +export default ThemeProvider diff --git a/packages/chakra-ui-core/src/ThemeProvider/index.js b/packages/chakra-ui-core/src/ThemeProvider/index.js index cb6619c3..3ccb0787 100644 --- a/packages/chakra-ui-core/src/ThemeProvider/index.js +++ b/packages/chakra-ui-core/src/ThemeProvider/index.js @@ -1,48 +1,2 @@ -import { colorModeObserver } from '../utils/color-mode-observer' - -const ThemeProvider = { - name: 'ThemeProvider', - provide () { - return { - $theme: () => this.theme, - $icons: this.icons, - /** - * By default the ThemeProvider exposes a colorMode value of light - * If no `ColorModeProvider` is provided in children/ consumer app, all chakra - * components will consume the $colorMode from here. - */ - $colorMode: () => 'light' - } - }, - computed: { - icons () { - return this.$chakra ? this.$chakra.icons : {} - }, - theme () { - return this.$chakra.theme - } - }, - watch: { - theme: { - immediate: true, - handler (newVal) { - colorModeObserver.theme = newVal - } - }, - icons: { - immediate: true, - handler (newVal) { - colorModeObserver.icons = newVal - } - } - }, - render (h) { - return h('div', { - attrs: { - id: '__chakra-app' - } - }, this.$slots.default) - } -} - +import ThemeProvider from './ThemeProvider' export default ThemeProvider diff --git a/packages/chakra-ui-core/src/Toast/Toast.js b/packages/chakra-ui-core/src/Toast/Toast.js new file mode 100644 index 00000000..942b9726 --- /dev/null +++ b/packages/chakra-ui-core/src/Toast/Toast.js @@ -0,0 +1,173 @@ +import Breadstick from 'breadstick' +import { Alert, AlertIcon, AlertTitle, AlertDescription } from '../Alert' +import Box from '../Box' +import CloseButton from '../CloseButton' +import ThemeProvider from '../ThemeProvider' +import { baseProps } from '../config/props' +import { forwardProps } from '../utils' +import ColorModeProvider from '../ColorModeProvider' +import { colorModeObserver } from '../utils/color-mode-observer' + +// Create breadstick instance. +const breadstick = new Breadstick() + +/** + * Toast component + */ +const Toast = { + name: 'Toast', + props: { + status: { + type: String, + default: 'info' + }, + variant: { + type: String, + default: 'solid' + }, + id: { + type: String + }, + title: { + type: String, + default: '' + }, + isClosable: { + type: Boolean, + default: true + }, + onClose: { + type: Function, + default: () => null + }, + description: { + type: String, + default: '' + }, + ...baseProps + }, + render (h) { + return h(Alert, { + props: { + status: this.status, + variant: this.variant, + textAlign: 'left', + boxShadow: 'lg', + rounded: 'md', + alignItems: 'start', + fontFamily: 'body', + m: 2, + pr: 2, + p: 4, + ...forwardProps(this.$props) + }, + attrs: { + id: this.id + } + }, [ + h(AlertIcon), + h(Box, { + props: { + flex: '1' + } + }, [ + this.title && h(AlertTitle, {}, this.title), + this.description && h(AlertDescription, {}, this.description) + ]), + this.isClosable && h(CloseButton, { + props: { + size: 'sm', + position: 'absolute', + right: '4px', + top: '4px', + color: 'currentColor' + }, + on: { + click: this.onClose + } + }) + ]) + } +} + +/** + * @description Toast initialization API + * TODO: In Vue 3 this should be exposed as a hook of it's own so as to + * to inject theme and icons variables provided by theme provider component. + */ +function useToast () { + const { theme } = colorModeObserver + /** + * @description Notify Method for Kiwi + * @param {Object} options + * @property {String} position + * @property {Number} duration + * @property {Function} render + * @property {String} title + * @property {String} description + * @property {String} status + * @property {String} variant + * @property {Boolean} isClosable + */ + function notify ({ + position = 'bottom', + duration = 5000, + render, + title, + description, + status, + variant = 'solid', + isClosable + }) { + const options = { + position, + duration + } + + if (render) { + return breadstick.notify( + ({ h, onClose, id }) => { + return h(ThemeProvider, { + props: { + theme + } + }, [render({ onClose, id })]) + }, + options + ) + } + + /** + * @todo Need to battletest breadstick to RELIABLY support JSX API and render function API globally. + */ + breadstick.notify(({ h, onClose, id }) => { + const { theme, colorMode, icons } = colorModeObserver + return h(ThemeProvider, { + props: { + icons, + theme + } + }, [h(ColorModeProvider, { + props: { + value: colorMode || 'light' + } + }, [h(Toast, { + props: { + status, + variant, + id: `${id}`, + title, + isClosable, + onClose, + description + } + })])]) + }, + options + ) + } + + return notify +} + +export default useToast diff --git a/packages/chakra-ui-core/src/Toast/index.js b/packages/chakra-ui-core/src/Toast/index.js index 942b9726..63f40c98 100644 --- a/packages/chakra-ui-core/src/Toast/index.js +++ b/packages/chakra-ui-core/src/Toast/index.js @@ -1,173 +1,2 @@ -import Breadstick from 'breadstick' -import { Alert, AlertIcon, AlertTitle, AlertDescription } from '../Alert' -import Box from '../Box' -import CloseButton from '../CloseButton' -import ThemeProvider from '../ThemeProvider' -import { baseProps } from '../config/props' -import { forwardProps } from '../utils' -import ColorModeProvider from '../ColorModeProvider' -import { colorModeObserver } from '../utils/color-mode-observer' - -// Create breadstick instance. -const breadstick = new Breadstick() - -/** - * Toast component - */ -const Toast = { - name: 'Toast', - props: { - status: { - type: String, - default: 'info' - }, - variant: { - type: String, - default: 'solid' - }, - id: { - type: String - }, - title: { - type: String, - default: '' - }, - isClosable: { - type: Boolean, - default: true - }, - onClose: { - type: Function, - default: () => null - }, - description: { - type: String, - default: '' - }, - ...baseProps - }, - render (h) { - return h(Alert, { - props: { - status: this.status, - variant: this.variant, - textAlign: 'left', - boxShadow: 'lg', - rounded: 'md', - alignItems: 'start', - fontFamily: 'body', - m: 2, - pr: 2, - p: 4, - ...forwardProps(this.$props) - }, - attrs: { - id: this.id - } - }, [ - h(AlertIcon), - h(Box, { - props: { - flex: '1' - } - }, [ - this.title && h(AlertTitle, {}, this.title), - this.description && h(AlertDescription, {}, this.description) - ]), - this.isClosable && h(CloseButton, { - props: { - size: 'sm', - position: 'absolute', - right: '4px', - top: '4px', - color: 'currentColor' - }, - on: { - click: this.onClose - } - }) - ]) - } -} - -/** - * @description Toast initialization API - * TODO: In Vue 3 this should be exposed as a hook of it's own so as to - * to inject theme and icons variables provided by theme provider component. - */ -function useToast () { - const { theme } = colorModeObserver - /** - * @description Notify Method for Kiwi - * @param {Object} options - * @property {String} position - * @property {Number} duration - * @property {Function} render - * @property {String} title - * @property {String} description - * @property {String} status - * @property {String} variant - * @property {Boolean} isClosable - */ - function notify ({ - position = 'bottom', - duration = 5000, - render, - title, - description, - status, - variant = 'solid', - isClosable - }) { - const options = { - position, - duration - } - - if (render) { - return breadstick.notify( - ({ h, onClose, id }) => { - return h(ThemeProvider, { - props: { - theme - } - }, [render({ onClose, id })]) - }, - options - ) - } - - /** - * @todo Need to battletest breadstick to RELIABLY support JSX API and render function API globally. - */ - breadstick.notify(({ h, onClose, id }) => { - const { theme, colorMode, icons } = colorModeObserver - return h(ThemeProvider, { - props: { - icons, - theme - } - }, [h(ColorModeProvider, { - props: { - value: colorMode || 'light' - } - }, [h(Toast, { - props: { - status, - variant, - id: `${id}`, - title, - isClosable, - onClose, - description - } - })])]) - }, - options - ) - } - - return notify -} - -export default useToast +import Toast from './Toast' +export default Toast diff --git a/packages/chakra-ui-core/src/Tooltip/Tooltip.js b/packages/chakra-ui-core/src/Tooltip/Tooltip.js new file mode 100644 index 00000000..28f78e7d --- /dev/null +++ b/packages/chakra-ui-core/src/Tooltip/Tooltip.js @@ -0,0 +1,216 @@ +import Fragment from '../Fragment' +import VisuallyHidden from '../VisuallyHidden' +import { Popper, PopperArrow } from '../Popper' +import { cloneVNode, useId, forwardProps, wrapEvent } from '../utils' +import { baseProps } from '../config/props' +import Box from '../Box' + +const tooltipProps = { + label: String, + _ariaLabel: String, + showDelay: { + type: Number, + default: 0 + }, + hideDelay: { + type: Number, + default: 0 + }, + placement: { + type: String, + default: 'top' + }, + hasArrow: Boolean, + closeOnClick: Boolean, + defaultIsOpen: Boolean, + shouldWrapChildren: Boolean, + controlledIsOpen: Boolean, + isControlled: Boolean, + onOpen: Function, + onClose: Function, + ...baseProps +} + +// TODO: Add isControlled support. +const Tooltip = { + inject: ['$colorMode'], + name: 'Tooltip', + data () { + return { + isOpen: this.isControlled ? this.controlledIsOpen : this.defaultIsOpen || false, + enterTimeout: null, + exitTimeout: null, + tooltipAnchor: undefined, + noop: 0 + } + }, + computed: { + colorMode () { + return this.$colorMode() + }, + tooltipId () { + return `tooltip-${useId(4)}` + } + }, + methods: { + open () { + this.isOpen = true + }, + close () { + this.isOpen = false + }, + openWithDelay () { + this.enterTimeout = setTimeout(this.open, this.showDelay) + }, + closeWithDelay () { + this.exitTimeout = setTimeout(this.close, this.hideDelay) + }, + handleOpen () { + if (!this.isControlled) { + this.openWithDelay() + } + this.open && this.open() + this.$emit('open') + }, + handleClose () { + if (!this.isControlled) { + this.closeWithDelay() + } + this.close && this.close() + this.$emit('close') + }, + handleClick () { + this.closeOnClick && this.closeOnClick() + this.$emit('click') + } + }, + props: tooltipProps, + mounted () { + // When component is mounted we force re-render because component + // children may not yet be rendered so event listeners may not be + // Attached immediately. + this.$nextTick(() => { + this.noop++ + this.tooltipAnchor = document.querySelector(`[x-tooltip-anchor=${this.tooltipId}]`) + }) + }, + render (h) { + let clone + + // Styles for tooltip + const _bg = this.colorMode === 'dark' ? 'gray.300' : 'gray.700' + const _color = this.colorMode === 'dark' ? 'gray.900' : 'whiteAlpha.900' + + // ARIA label for tooltip + const hasAriaLabel = this._ariaLabel !== undefined + + // Child nodes parsing + const children = this.$slots.default + if (children.length > 1) { + return console.error('[ChakraUI]: The Tooltip component only expects one child.') + } + if (children[0].text || this.shouldWrapChildren) { + clone = ( + clone = h(Box, { + props: { + as: 'span' + }, + attrs: { + tabIndex: 0, + 'x-tooltip-anchor': `${this.tooltipId}`, + ...(this.isOpen && { 'aria-describedby': this.tooltipId }) + }, + on: { + mouseenter: this.handleOpen, + mouseleave: this.handleClose, + click: this.handleClick, + focus: this.handleOpen, + blur: this.handleClose + }, + ref: 'tooltipRef' + }, children[0].text) + ) + } else { + const cloned = cloneVNode(children[0], h) + if (cloned.componentOptions) { + /** + * For now consumer's need to use `.native` modifier on events + * because we're cloning vnodes and I presently do not know how + * to capture those events and log them. + * + * In the future it will be good to implement such. + * -> We'd like to be able to wrap cloned VNode events with our + * internal tooltips events. + */ + clone = h(cloned.componentOptions.Ctor, { + ...cloned.data, + ...(cloned.componentOptions.listeners || {}), + props: { + ...(cloned.data.props || {}), + ...cloned.componentOptions.propsData + }, + attrs: { + ...cloned.data.attrs, + 'x-tooltip-anchor': `${this.tooltipId}` + }, + on: cloned.componentOptions.listeners, + nativeOn: { + 'mouseenter': this.handleOpen, + 'mouseleave': this.handleClose, + 'click': wrapEvent(this.handleClick, (e) => this.$emit('click', e)), + 'focus': this.handleOpen, + 'blur': this.handleClose + } + }, cloned.componentOptions.children) + } + } + + return h(Fragment, [ + clone, + h(Popper, { + props: { + usePortal: true, + anchorEl: this.tooltipAnchor, + hasArrow: true, + isOpen: this.isOpen, + placement: this.placement, + modifiers: { + offset: { + enabled: true, + offset: '0, 8' + } + }, + arrowSize: '10px', + px: '8px', + py: '2px', + _id: this.tooltipId, + bg: _bg, + borderRadius: 'sm', + fontWeight: 'medium', + pointerEvents: 'none', + color: _color, + fontSize: 'sm', + shadow: 'md', + maxW: '320px', + ...forwardProps(this.$props) + }, + attrs: { + id: hasAriaLabel ? undefined : this.tooltipId, + role: hasAriaLabel ? undefined : 'tooltip', + 'data-noop': this.noop + } + }, [ + this.label, + hasAriaLabel && h(VisuallyHidden, { + attrs: { + role: 'tooltip', + id: this.tooltipId + } + }, this._ariaLabel), + this.hasArrow && h(PopperArrow) + ]) + ]) + } +} + +export default Tooltip diff --git a/packages/chakra-ui-core/src/Tooltip/index.js b/packages/chakra-ui-core/src/Tooltip/index.js index 28f78e7d..4c8e6b9e 100644 --- a/packages/chakra-ui-core/src/Tooltip/index.js +++ b/packages/chakra-ui-core/src/Tooltip/index.js @@ -1,216 +1,2 @@ -import Fragment from '../Fragment' -import VisuallyHidden from '../VisuallyHidden' -import { Popper, PopperArrow } from '../Popper' -import { cloneVNode, useId, forwardProps, wrapEvent } from '../utils' -import { baseProps } from '../config/props' -import Box from '../Box' - -const tooltipProps = { - label: String, - _ariaLabel: String, - showDelay: { - type: Number, - default: 0 - }, - hideDelay: { - type: Number, - default: 0 - }, - placement: { - type: String, - default: 'top' - }, - hasArrow: Boolean, - closeOnClick: Boolean, - defaultIsOpen: Boolean, - shouldWrapChildren: Boolean, - controlledIsOpen: Boolean, - isControlled: Boolean, - onOpen: Function, - onClose: Function, - ...baseProps -} - -// TODO: Add isControlled support. -const Tooltip = { - inject: ['$colorMode'], - name: 'Tooltip', - data () { - return { - isOpen: this.isControlled ? this.controlledIsOpen : this.defaultIsOpen || false, - enterTimeout: null, - exitTimeout: null, - tooltipAnchor: undefined, - noop: 0 - } - }, - computed: { - colorMode () { - return this.$colorMode() - }, - tooltipId () { - return `tooltip-${useId(4)}` - } - }, - methods: { - open () { - this.isOpen = true - }, - close () { - this.isOpen = false - }, - openWithDelay () { - this.enterTimeout = setTimeout(this.open, this.showDelay) - }, - closeWithDelay () { - this.exitTimeout = setTimeout(this.close, this.hideDelay) - }, - handleOpen () { - if (!this.isControlled) { - this.openWithDelay() - } - this.open && this.open() - this.$emit('open') - }, - handleClose () { - if (!this.isControlled) { - this.closeWithDelay() - } - this.close && this.close() - this.$emit('close') - }, - handleClick () { - this.closeOnClick && this.closeOnClick() - this.$emit('click') - } - }, - props: tooltipProps, - mounted () { - // When component is mounted we force re-render because component - // children may not yet be rendered so event listeners may not be - // Attached immediately. - this.$nextTick(() => { - this.noop++ - this.tooltipAnchor = document.querySelector(`[x-tooltip-anchor=${this.tooltipId}]`) - }) - }, - render (h) { - let clone - - // Styles for tooltip - const _bg = this.colorMode === 'dark' ? 'gray.300' : 'gray.700' - const _color = this.colorMode === 'dark' ? 'gray.900' : 'whiteAlpha.900' - - // ARIA label for tooltip - const hasAriaLabel = this._ariaLabel !== undefined - - // Child nodes parsing - const children = this.$slots.default - if (children.length > 1) { - return console.error('[ChakraUI]: The Tooltip component only expects one child.') - } - if (children[0].text || this.shouldWrapChildren) { - clone = ( - clone = h(Box, { - props: { - as: 'span' - }, - attrs: { - tabIndex: 0, - 'x-tooltip-anchor': `${this.tooltipId}`, - ...(this.isOpen && { 'aria-describedby': this.tooltipId }) - }, - on: { - mouseenter: this.handleOpen, - mouseleave: this.handleClose, - click: this.handleClick, - focus: this.handleOpen, - blur: this.handleClose - }, - ref: 'tooltipRef' - }, children[0].text) - ) - } else { - const cloned = cloneVNode(children[0], h) - if (cloned.componentOptions) { - /** - * For now consumer's need to use `.native` modifier on events - * because we're cloning vnodes and I presently do not know how - * to capture those events and log them. - * - * In the future it will be good to implement such. - * -> We'd like to be able to wrap cloned VNode events with our - * internal tooltips events. - */ - clone = h(cloned.componentOptions.Ctor, { - ...cloned.data, - ...(cloned.componentOptions.listeners || {}), - props: { - ...(cloned.data.props || {}), - ...cloned.componentOptions.propsData - }, - attrs: { - ...cloned.data.attrs, - 'x-tooltip-anchor': `${this.tooltipId}` - }, - on: cloned.componentOptions.listeners, - nativeOn: { - 'mouseenter': this.handleOpen, - 'mouseleave': this.handleClose, - 'click': wrapEvent(this.handleClick, (e) => this.$emit('click', e)), - 'focus': this.handleOpen, - 'blur': this.handleClose - } - }, cloned.componentOptions.children) - } - } - - return h(Fragment, [ - clone, - h(Popper, { - props: { - usePortal: true, - anchorEl: this.tooltipAnchor, - hasArrow: true, - isOpen: this.isOpen, - placement: this.placement, - modifiers: { - offset: { - enabled: true, - offset: '0, 8' - } - }, - arrowSize: '10px', - px: '8px', - py: '2px', - _id: this.tooltipId, - bg: _bg, - borderRadius: 'sm', - fontWeight: 'medium', - pointerEvents: 'none', - color: _color, - fontSize: 'sm', - shadow: 'md', - maxW: '320px', - ...forwardProps(this.$props) - }, - attrs: { - id: hasAriaLabel ? undefined : this.tooltipId, - role: hasAriaLabel ? undefined : 'tooltip', - 'data-noop': this.noop - } - }, [ - this.label, - hasAriaLabel && h(VisuallyHidden, { - attrs: { - role: 'tooltip', - id: this.tooltipId - } - }, this._ariaLabel), - this.hasArrow && h(PopperArrow) - ]) - ]) - } -} - +import Tooltip from './Tooltip.js' export default Tooltip diff --git a/packages/chakra-ui-core/src/Transition/Transition.js b/packages/chakra-ui-core/src/Transition/Transition.js new file mode 100644 index 00000000..86783f15 --- /dev/null +++ b/packages/chakra-ui-core/src/Transition/Transition.js @@ -0,0 +1,534 @@ +import anime from 'animejs' +import { isUndef, isVueComponent, cloneVNodeElement, cleanChildren } from '../utils' +import Box from '../Box' + +const enterEasing = 'spring(1, 100, 50, 0)' +const leaveEasing = 'spring(1, 100, 70, 0)' + +const Slide = { + name: 'Slide', + props: { + initialHeight: { + type: Number, + default: 0 + }, + duration: { + type: Number, + default: 150 + }, + enterEasing: { + type: String, + default: enterEasing + }, + leaveEasing: { + type: String, + default: leaveEasing + }, + finalHeight: Number, + animateOpacity: { + type: Boolean, + default: true + }, + from: { + type: String, + default: 'bottom' + } + }, + data () { + return { + transitionOptions: { + bottom: { + offset: '-100%', + transform: 'translateY' + }, + top: { + offset: '100%', + transform: 'translateY' + }, + left: { + offset: '100%', + transform: 'translateX' + }, + right: { + offset: '-100%', + transform: 'translateX' + } + } + } + }, + computed: { + transform () { + return this.transitionOptions[this.from].transform + }, + transitions () { + return { + enter: { + [this.transform]: ['0%', this.transitionOptions[this.from]['offset']], + opacity: [0, 1] + }, + leave: { + [this.transform]: [this.transitionOptions[this.from]['offset'], '0%'], + opacity: 0 + } + } + } + }, + methods: { + enter (el, complete) { + anime({ + targets: el, + ...this.transitions['enter'], + complete, + easing: this.enterEasing + }) + }, + + leave (el, complete) { + anime({ + targets: el, + ...this.transitions['leave'], + complete, + easing: this.leaveEasing + }) + } + }, + render (h) { + if (isUndef(this.from)) { + console.error('[Chakra]: The Slide component expected prop "from" but none was passed.') + return () => null + } + + const children = this.$slots.default + const TransitionElement = children.length > 1 ? 'TransitionGroup' : 'Transition' + return h(TransitionElement, { + props: { + css: false + }, + on: { + beforeEnter (el) { + el && el.style.setProperty('will-change', 'opacity, transform') + }, + enter: this.enter, + leave: this.enter + } + }, this.$slots.default) + } +} + +const Scale = { + name: 'Scale', + props: { + initialHeight: { + type: Number, + default: 0 + }, + duration: { + type: Number, + default: 150 + }, + enterEasing: { + type: String, + default: enterEasing + }, + leaveEasing: { + type: String, + default: leaveEasing + }, + finalHeight: Number, + animateOpacity: { + type: Boolean, + default: true + } + }, + methods: { + enter (el, complete) { + anime({ + targets: el, + opacity: [0, 1], + scale: [this.initialScale, 1], + easing: this.enterEasing, + duration: this.duration, + complete + }) + }, + leave (el, complete) { + anime({ + targets: el, + opacity: [1, 0], + scale: [1, this.initialScale], + easing: this.leaveEasing, + duration: this.duration, + complete + }) + } + }, + render (h) { + let finalChildren + const children = this.$slots.default || [h(null)] + + if (children.length > 1) { + const clean = cleanChildren(children) + finalChildren = clean.map((vnode, index) => { + return cloneVNodeElement(vnode, { + key: `scale-${index}` + }, h) + }) + } else { + finalChildren = children + } + + const TransitionElement = finalChildren.length > 1 ? 'TransitionGroup' : 'Transition' + + return h(TransitionElement, { + props: { + css: false + }, + on: { + beforeEnter (el) { + el && el.style.setProperty('will-change', 'opacity, transform') + }, + enter: this.enter, + leave: this.leave + } + }, finalChildren) + } +} + +const Fade = { + name: 'Fade', + props: { + initialHeight: { + type: Number, + default: 0 + }, + duration: { + type: Number, + default: 150 + }, + enterEasing: { + type: String, + default: enterEasing + }, + leaveEasing: { + type: String, + default: leaveEasing + }, + finalHeight: Number, + animateOpacity: { + type: Boolean, + default: true + } + }, + methods: { + enter (el, complete) { + anime({ + targets: el, + opacity: [0, 1], + easing: this.enterEasing, + duration: this.duration, + complete + }) + }, + leave (el, complete) { + anime({ + targets: el, + opacity: [1, 0], + easing: this.leaveEasing, + duration: this.duration, + complete + }) + } + }, + render (h) { + let finalChildren + const children = this.$slots.default || [h(null)] + + if (children.length > 1) { + const clean = cleanChildren(children) + finalChildren = clean.map((vnode, index) => { + return cloneVNodeElement(vnode, { + key: `scale-${index}` + }, h) + }) + } else { + finalChildren = children + } + + const TransitionElement = finalChildren.length > 1 ? 'TransitionGroup' : 'Transition' + + return h(TransitionElement, { + props: { + css: false + }, + on: { + beforeEnter (el) { + el && el.style.setProperty('will-change', 'opacity, transform') + }, + enter: this.enter, + leave: this.leave + } + }, finalChildren) + } +} + +const SlideIn = { + name: 'SlideIn', + props: { + offset: { + type: String, + default: '10px' + }, + duration: { + type: Number, + default: 150 + } + }, + methods: { + enter (el, complete) { + anime({ + targets: el, + opacity: [0, 1], + translateY: [this.offset, '0px'], + easing: enterEasing, + complete + }) + }, + leave (el, complete) { + anime({ + targets: el, + opacity: [1, 0], + translateY: ['0px', this.offset], + easing: leaveEasing, + complete + }) + } + }, + render (h) { + let finalChildren + const children = this.$slots.default || [h(null)] + + if (children.length > 1) { + const clean = cleanChildren(children) + finalChildren = clean.map((vnode, index) => { + return cloneVNodeElement(vnode, { + key: `scale-${index}` + }, h) + }) + } else { + finalChildren = children + } + + const TransitionElement = finalChildren.length > 1 ? 'TransitionGroup' : 'Transition' + + return h(TransitionElement, { + props: { + css: false + }, + on: { + beforeEnter (el) { + el && el.style.setProperty('will-change', 'opacity, transform') + }, + enter: this.enter, + leave: this.leave + } + }, finalChildren) + } +} + +const RevealHeight = { + name: 'RevealHeight', + props: { + initialHeight: { + type: Number, + default: 0 + }, + duration: { + type: Number, + default: 150 + }, + enterEasing: { + type: String, + default: enterEasing + }, + leaveEasing: { + type: String, + default: leaveEasing + }, + finalHeight: Number, + animateOpacity: { + type: Boolean, + default: true + } + }, + methods: { + enter (el, complete) { + this.$emit('enter', el) + el.style.visibility = 'hidden' + el.style.height = 'auto' + const { height } = getComputedStyle(el) + el.style.height = this.initialHeight || 0 + + requestAnimationFrame(() => { + el.style.visibility = 'visible' + anime({ + targets: el, + ...this.animateOpacity && { opacity: [0, 1] }, + height: [this.initialHeight || 0, this.finalHeight || height], + easing: this.enterEasing, + duration: this.duration, + complete + }) + }) + }, + leave (el, complete) { + this.$emit('leave', el) + const { height } = getComputedStyle(el) + + requestAnimationFrame(() => { + anime({ + targets: el, + ...this.animateOpacity && { opacity: [1, 0] }, + height: [this.finalHeight || height, this.initialHeight || 0], + easing: this.leaveEasing, + duration: this.duration, + complete + }) + }) + }, + handleEmit (event, payload) { + this.$emit(event, payload) + } + }, + render (h) { + const children = this.$slots.default + if (!children) return h() + const TransitionElement = children ? children.length > 1 ? 'TransitionGroup' : 'Transition' : 'Transition' + const clones = children.map((vnode, index) => { + return cloneVNodeElement(vnode, { + key: `scale-${index}` + }) + }) + + return h(TransitionElement, { + props: { + css: false + }, + on: { + beforeEnter: (el) => { + if (el) { + el.style.setProperty('will-change', 'opacity, transform') + } + this.handleEmit('beforeEnter', el) + }, + enter: this.enter, + leave: this.leave, + afterEnter: (el) => { + el.style.height = 'auto' + this.handleEmit('afterEnter', el) + } + } + }, clones) + } +} + +const AnimateHeight = { + name: 'AnimateHeight', + props: { + isOpen: Boolean, + initialHeight: { + type: Number, + default: 0 + }, + duration: { + type: Number, + default: 150 + }, + enterEasing: { + type: String, + default: enterEasing + }, + leaveEasing: { + type: String, + default: leaveEasing + }, + finalHeight: Number, + animateOpacity: { + type: Boolean, + default: true + } + }, + data () { + return { + el: undefined + } + }, + mounted () { + this.el = this.getNode(this.$el) + this.$watch('isOpen', (isOpen) => { + if (isOpen) this.enter(this.el, () => {}) + else this.leave(this.el, () => {}) + }, { + immediate: true + }) + }, + methods: { + enter (el, complete) { + this.$emit('enter', el) + el.style.visibility = 'hidden' + el.style.height = this.finalHeight || 'auto' + const { height } = getComputedStyle(el) + el.style.height = this.initialHeight || 0 + + requestAnimationFrame(() => { + el.style.visibility = 'visible' + anime({ + targets: el, + ...this.animateOpacity && { opacity: [0, 1] }, + height: [this.initialHeight || 0, this.finalHeight || height], + easing: this.enterEasing, + duration: this.duration, + complete + }) + }) + }, + leave (el, complete) { + this.$emit('leave', el) + const { height } = getComputedStyle(el) + + requestAnimationFrame(() => { + anime({ + targets: el, + ...this.animateOpacity && { opacity: [1, 0] }, + height: [this.finalHeight || height, this.initialHeight || 0], + easing: this.leaveEasing, + duration: this.duration, + complete + }) + }) + }, + handleEmit (event, payload) { + this.$emit(event, payload) + }, + getNode (element) { + const isVue = isVueComponent(element) + return isVue ? element.$el : element + } + }, + render (h) { + const children = this.$slots.default + return h(Box, { + props: { + overflow: 'hidden' + } + }, children) + } +} + +export { + Slide, + Scale, + SlideIn, + AnimateHeight, + RevealHeight, + Fade +} diff --git a/packages/chakra-ui-core/src/Transition/index.js b/packages/chakra-ui-core/src/Transition/index.js index 86783f15..bf68788c 100644 --- a/packages/chakra-ui-core/src/Transition/index.js +++ b/packages/chakra-ui-core/src/Transition/index.js @@ -1,534 +1 @@ -import anime from 'animejs' -import { isUndef, isVueComponent, cloneVNodeElement, cleanChildren } from '../utils' -import Box from '../Box' - -const enterEasing = 'spring(1, 100, 50, 0)' -const leaveEasing = 'spring(1, 100, 70, 0)' - -const Slide = { - name: 'Slide', - props: { - initialHeight: { - type: Number, - default: 0 - }, - duration: { - type: Number, - default: 150 - }, - enterEasing: { - type: String, - default: enterEasing - }, - leaveEasing: { - type: String, - default: leaveEasing - }, - finalHeight: Number, - animateOpacity: { - type: Boolean, - default: true - }, - from: { - type: String, - default: 'bottom' - } - }, - data () { - return { - transitionOptions: { - bottom: { - offset: '-100%', - transform: 'translateY' - }, - top: { - offset: '100%', - transform: 'translateY' - }, - left: { - offset: '100%', - transform: 'translateX' - }, - right: { - offset: '-100%', - transform: 'translateX' - } - } - } - }, - computed: { - transform () { - return this.transitionOptions[this.from].transform - }, - transitions () { - return { - enter: { - [this.transform]: ['0%', this.transitionOptions[this.from]['offset']], - opacity: [0, 1] - }, - leave: { - [this.transform]: [this.transitionOptions[this.from]['offset'], '0%'], - opacity: 0 - } - } - } - }, - methods: { - enter (el, complete) { - anime({ - targets: el, - ...this.transitions['enter'], - complete, - easing: this.enterEasing - }) - }, - - leave (el, complete) { - anime({ - targets: el, - ...this.transitions['leave'], - complete, - easing: this.leaveEasing - }) - } - }, - render (h) { - if (isUndef(this.from)) { - console.error('[Chakra]: The Slide component expected prop "from" but none was passed.') - return () => null - } - - const children = this.$slots.default - const TransitionElement = children.length > 1 ? 'TransitionGroup' : 'Transition' - return h(TransitionElement, { - props: { - css: false - }, - on: { - beforeEnter (el) { - el && el.style.setProperty('will-change', 'opacity, transform') - }, - enter: this.enter, - leave: this.enter - } - }, this.$slots.default) - } -} - -const Scale = { - name: 'Scale', - props: { - initialHeight: { - type: Number, - default: 0 - }, - duration: { - type: Number, - default: 150 - }, - enterEasing: { - type: String, - default: enterEasing - }, - leaveEasing: { - type: String, - default: leaveEasing - }, - finalHeight: Number, - animateOpacity: { - type: Boolean, - default: true - } - }, - methods: { - enter (el, complete) { - anime({ - targets: el, - opacity: [0, 1], - scale: [this.initialScale, 1], - easing: this.enterEasing, - duration: this.duration, - complete - }) - }, - leave (el, complete) { - anime({ - targets: el, - opacity: [1, 0], - scale: [1, this.initialScale], - easing: this.leaveEasing, - duration: this.duration, - complete - }) - } - }, - render (h) { - let finalChildren - const children = this.$slots.default || [h(null)] - - if (children.length > 1) { - const clean = cleanChildren(children) - finalChildren = clean.map((vnode, index) => { - return cloneVNodeElement(vnode, { - key: `scale-${index}` - }, h) - }) - } else { - finalChildren = children - } - - const TransitionElement = finalChildren.length > 1 ? 'TransitionGroup' : 'Transition' - - return h(TransitionElement, { - props: { - css: false - }, - on: { - beforeEnter (el) { - el && el.style.setProperty('will-change', 'opacity, transform') - }, - enter: this.enter, - leave: this.leave - } - }, finalChildren) - } -} - -const Fade = { - name: 'Fade', - props: { - initialHeight: { - type: Number, - default: 0 - }, - duration: { - type: Number, - default: 150 - }, - enterEasing: { - type: String, - default: enterEasing - }, - leaveEasing: { - type: String, - default: leaveEasing - }, - finalHeight: Number, - animateOpacity: { - type: Boolean, - default: true - } - }, - methods: { - enter (el, complete) { - anime({ - targets: el, - opacity: [0, 1], - easing: this.enterEasing, - duration: this.duration, - complete - }) - }, - leave (el, complete) { - anime({ - targets: el, - opacity: [1, 0], - easing: this.leaveEasing, - duration: this.duration, - complete - }) - } - }, - render (h) { - let finalChildren - const children = this.$slots.default || [h(null)] - - if (children.length > 1) { - const clean = cleanChildren(children) - finalChildren = clean.map((vnode, index) => { - return cloneVNodeElement(vnode, { - key: `scale-${index}` - }, h) - }) - } else { - finalChildren = children - } - - const TransitionElement = finalChildren.length > 1 ? 'TransitionGroup' : 'Transition' - - return h(TransitionElement, { - props: { - css: false - }, - on: { - beforeEnter (el) { - el && el.style.setProperty('will-change', 'opacity, transform') - }, - enter: this.enter, - leave: this.leave - } - }, finalChildren) - } -} - -const SlideIn = { - name: 'SlideIn', - props: { - offset: { - type: String, - default: '10px' - }, - duration: { - type: Number, - default: 150 - } - }, - methods: { - enter (el, complete) { - anime({ - targets: el, - opacity: [0, 1], - translateY: [this.offset, '0px'], - easing: enterEasing, - complete - }) - }, - leave (el, complete) { - anime({ - targets: el, - opacity: [1, 0], - translateY: ['0px', this.offset], - easing: leaveEasing, - complete - }) - } - }, - render (h) { - let finalChildren - const children = this.$slots.default || [h(null)] - - if (children.length > 1) { - const clean = cleanChildren(children) - finalChildren = clean.map((vnode, index) => { - return cloneVNodeElement(vnode, { - key: `scale-${index}` - }, h) - }) - } else { - finalChildren = children - } - - const TransitionElement = finalChildren.length > 1 ? 'TransitionGroup' : 'Transition' - - return h(TransitionElement, { - props: { - css: false - }, - on: { - beforeEnter (el) { - el && el.style.setProperty('will-change', 'opacity, transform') - }, - enter: this.enter, - leave: this.leave - } - }, finalChildren) - } -} - -const RevealHeight = { - name: 'RevealHeight', - props: { - initialHeight: { - type: Number, - default: 0 - }, - duration: { - type: Number, - default: 150 - }, - enterEasing: { - type: String, - default: enterEasing - }, - leaveEasing: { - type: String, - default: leaveEasing - }, - finalHeight: Number, - animateOpacity: { - type: Boolean, - default: true - } - }, - methods: { - enter (el, complete) { - this.$emit('enter', el) - el.style.visibility = 'hidden' - el.style.height = 'auto' - const { height } = getComputedStyle(el) - el.style.height = this.initialHeight || 0 - - requestAnimationFrame(() => { - el.style.visibility = 'visible' - anime({ - targets: el, - ...this.animateOpacity && { opacity: [0, 1] }, - height: [this.initialHeight || 0, this.finalHeight || height], - easing: this.enterEasing, - duration: this.duration, - complete - }) - }) - }, - leave (el, complete) { - this.$emit('leave', el) - const { height } = getComputedStyle(el) - - requestAnimationFrame(() => { - anime({ - targets: el, - ...this.animateOpacity && { opacity: [1, 0] }, - height: [this.finalHeight || height, this.initialHeight || 0], - easing: this.leaveEasing, - duration: this.duration, - complete - }) - }) - }, - handleEmit (event, payload) { - this.$emit(event, payload) - } - }, - render (h) { - const children = this.$slots.default - if (!children) return h() - const TransitionElement = children ? children.length > 1 ? 'TransitionGroup' : 'Transition' : 'Transition' - const clones = children.map((vnode, index) => { - return cloneVNodeElement(vnode, { - key: `scale-${index}` - }) - }) - - return h(TransitionElement, { - props: { - css: false - }, - on: { - beforeEnter: (el) => { - if (el) { - el.style.setProperty('will-change', 'opacity, transform') - } - this.handleEmit('beforeEnter', el) - }, - enter: this.enter, - leave: this.leave, - afterEnter: (el) => { - el.style.height = 'auto' - this.handleEmit('afterEnter', el) - } - } - }, clones) - } -} - -const AnimateHeight = { - name: 'AnimateHeight', - props: { - isOpen: Boolean, - initialHeight: { - type: Number, - default: 0 - }, - duration: { - type: Number, - default: 150 - }, - enterEasing: { - type: String, - default: enterEasing - }, - leaveEasing: { - type: String, - default: leaveEasing - }, - finalHeight: Number, - animateOpacity: { - type: Boolean, - default: true - } - }, - data () { - return { - el: undefined - } - }, - mounted () { - this.el = this.getNode(this.$el) - this.$watch('isOpen', (isOpen) => { - if (isOpen) this.enter(this.el, () => {}) - else this.leave(this.el, () => {}) - }, { - immediate: true - }) - }, - methods: { - enter (el, complete) { - this.$emit('enter', el) - el.style.visibility = 'hidden' - el.style.height = this.finalHeight || 'auto' - const { height } = getComputedStyle(el) - el.style.height = this.initialHeight || 0 - - requestAnimationFrame(() => { - el.style.visibility = 'visible' - anime({ - targets: el, - ...this.animateOpacity && { opacity: [0, 1] }, - height: [this.initialHeight || 0, this.finalHeight || height], - easing: this.enterEasing, - duration: this.duration, - complete - }) - }) - }, - leave (el, complete) { - this.$emit('leave', el) - const { height } = getComputedStyle(el) - - requestAnimationFrame(() => { - anime({ - targets: el, - ...this.animateOpacity && { opacity: [1, 0] }, - height: [this.finalHeight || height, this.initialHeight || 0], - easing: this.leaveEasing, - duration: this.duration, - complete - }) - }) - }, - handleEmit (event, payload) { - this.$emit(event, payload) - }, - getNode (element) { - const isVue = isVueComponent(element) - return isVue ? element.$el : element - } - }, - render (h) { - const children = this.$slots.default - return h(Box, { - props: { - overflow: 'hidden' - } - }, children) - } -} - -export { - Slide, - Scale, - SlideIn, - AnimateHeight, - RevealHeight, - Fade -} +export * from './Transition' diff --git a/packages/chakra-ui-core/src/VisuallyHidden/VisuallyHidden.js b/packages/chakra-ui-core/src/VisuallyHidden/VisuallyHidden.js new file mode 100644 index 00000000..fa46e80e --- /dev/null +++ b/packages/chakra-ui-core/src/VisuallyHidden/VisuallyHidden.js @@ -0,0 +1,41 @@ +import { css } from 'emotion' +import Box from '../Box' + +const VisuallyHidden = { + name: 'VisuallyHidden', + props: { + as: { + type: String, + default: 'div' + }, + w: [String, Number], + h: [String, Number], + pos: String + }, + computed: { + className () { + return css({ + border: '0px', + clip: 'rect(0px, 0px, 0px, 0px)', + height: `${this.w || '1px'}`, + width: `${this.h || '1px'}`, + margin: '-1px', + padding: '0px', + overflow: 'hidden', + whiteSpace: 'nowrap', + position: `${this.pos || 'absolute'}` + }) + } + }, + render (h) { + return h(Box, { + class: [this.className], + props: { + as: this.as + }, + attrs: this.$attrs + }, this.$slots.default) + } +} + +export default VisuallyHidden diff --git a/packages/chakra-ui-core/src/VisuallyHidden/index.js b/packages/chakra-ui-core/src/VisuallyHidden/index.js index fa46e80e..f43aee4f 100644 --- a/packages/chakra-ui-core/src/VisuallyHidden/index.js +++ b/packages/chakra-ui-core/src/VisuallyHidden/index.js @@ -1,41 +1,2 @@ -import { css } from 'emotion' -import Box from '../Box' - -const VisuallyHidden = { - name: 'VisuallyHidden', - props: { - as: { - type: String, - default: 'div' - }, - w: [String, Number], - h: [String, Number], - pos: String - }, - computed: { - className () { - return css({ - border: '0px', - clip: 'rect(0px, 0px, 0px, 0px)', - height: `${this.w || '1px'}`, - width: `${this.h || '1px'}`, - margin: '-1px', - padding: '0px', - overflow: 'hidden', - whiteSpace: 'nowrap', - position: `${this.pos || 'absolute'}` - }) - } - }, - render (h) { - return h(Box, { - class: [this.className], - props: { - as: this.as - }, - attrs: this.$attrs - }, this.$slots.default) - } -} - +import VisuallyHidden from './VisuallyHidden' export default VisuallyHidden