diff --git a/.dumirc.ts b/.dumirc.ts
index bd80941..141e73d 100644
--- a/.dumirc.ts
+++ b/.dumirc.ts
@@ -3,21 +3,12 @@ import path from 'path';
export default defineConfig({
alias: {
- 'rc-trigger$': path.resolve('src'),
- 'rc-trigger/es': path.resolve('src'),
+ 'rc-qrcode$': path.resolve('src'),
+ 'rc-qrcode/es': path.resolve('src'),
},
- mfsu: false,
favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'],
themeConfig: {
- name: 'Trigger',
+ name: 'Qrcode',
logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4',
},
- styles: [
- `
- .dumi-default-previewer-demo {
- position: relative;
- min-height: 300px;
- }
- `,
- ]
});
diff --git a/.fatherrc.js b/.fatherrc.js
index 4ddbafd..7414da0 100644
--- a/.fatherrc.js
+++ b/.fatherrc.js
@@ -1,5 +1,10 @@
import { defineConfig } from 'father';
export default defineConfig({
- plugins: ['@rc-component/father-plugin'],
-});
\ No newline at end of file
+ platform: 'browser',
+ cjs: { output: 'lib' },
+ esm: {
+ output: 'es',
+ alias: { 'rc-util/lib': 'rc-util/es' },
+ },
+});
diff --git a/HISTORY.md b/HISTORY.md
index 7ee63c8..fe11a72 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,58 +1,6 @@
# History
----
-## 4.1.0 / 2020-05-08
+## 1.0.0
-- upgrade rc-animate to `3.x`
-
-## 2.5.0 / 2018-06-05
-
-- support `alignPoint`
-
-## 2.1.0 / 2017-10-16
-
-- add action `contextMenu`
-
-## 2.0.0 / 2017-09-25
-
-- support React 16
-
-## 1.11.0 / 2017-06-07
-
-- add es
-
-## 1.9.0 / 2017-02-27
-
-- add getDocument prop
-
-## 1.8.2 / 2017-02-24
-
-- change default container to absolute to fix scrollbar change problem
-
-## 1.7.0 / 2016-07-18
-
-- use getContainerRenderMixin from 'rc-util'
-
-## 1.6.0 / 2016-05-26
-
-- support popup as function
-
-## 1.5.0 / 2016-05-26
-
-- add forcePopupAlign method
-
-## 1.4.0 / 2016-04-06
-
-- support onPopupAlign
-
-## 1.3.0 / 2016-03-25
-
-- support mask/maskTransitionName/zIndex
-
-## 1.2.0 / 2016-03-01
-
-- add showAction/hideAction
-
-## 1.1.0 / 2016-01-06
-
-- add root trigger node as parameter of getPopupContainer
+- feat: initial project
diff --git a/README.md b/README.md
index e479e56..e912959 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# @rc-component/trigger
+# @rc-component/QRCode
-React Trigger Component
+React QRCode Component
[![NPM version][npm-image]][npm-url]
[![npm download][download-image]][download-url]
@@ -9,59 +9,15 @@ React Trigger Component
[![bundle size][bundlephobia-image]][bundlephobia-url]
[![dumi][dumi-image]][dumi-url]
-[npm-image]: http://img.shields.io/npm/v/rc-checkbox.svg?style=flat-square
-[npm-url]: http://npmjs.org/package/rc-checkbox
-[github-actions-image]: https://github.com/react-component/checkbox/workflows/CI/badge.svg
-[github-actions-url]: https://github.com/react-component/checkbox/actions
-[codecov-image]: https://img.shields.io/codecov/c/github/react-component/checkbox/master.svg?style=flat-square
-[codecov-url]: https://codecov.io/gh/react-component/checkbox/branch/master
-[david-url]: https://david-dm.org/react-component/checkbox
-[david-image]: https://david-dm.org/react-component/checkbox/status.svg?style=flat-square
-[david-dev-url]: https://david-dm.org/react-component/checkbox?type=dev
-[david-dev-image]: https://david-dm.org/react-component/checkbox/dev-status.svg?style=flat-square
-[download-image]: https://img.shields.io/npm/dm/rc-checkbox.svg?style=flat-square
-[download-url]: https://npmjs.org/package/rc-checkbox
-[bundlephobia-url]: https://bundlephobia.com/result?p=rc-checkbox
-[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-checkbox
-[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
-[dumi-url]: https://github.com/umijs/dumi
-
## Install
-[![@rc-component/trigger](https://nodei.co/npm/@rc-component/trigger.png)](https://npmjs.org/package/@rc-component/trigger)
+[![@rc-component/qrcode](https://nodei.co/npm/@rc-component/qrcode.png)](https://npmjs.org/package/@rc-component/qrcode)
## Usage
-Include the default [styling](https://github.com/react-component/trigger/blob/master/assets/index.less#L4:L11) and then:
-
-```js
-import React from 'react';
-import ReactDOM from 'react-dom';
-import Trigger from '@rc-component/trigger';
-
-ReactDOM.render((
- popup}
- popupAlign={{
- points: ['tl', 'bl'],
- offset: [0, 3]
- }}
- >
- hover
-
-), container);
-```
-
-## Compatibility
-
-| [](http://godban.github.io/browsers-support-badges/)
IE / Edge | [](http://godban.github.io/browsers-support-badges/)
Firefox | [](http://godban.github.io/browsers-support-badges/)
Chrome | [](http://godban.github.io/browsers-support-badges/)
Safari | [](http://godban.github.io/browsers-support-badges/)
Electron |
-| --- | --- | --- | --- | --- |
-| IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
-
## Example
-http://localhost:9001
+http://localhost:8001
## Development
@@ -74,175 +30,11 @@ npm start
### props
-
-
-
- name |
- type |
- default |
- description |
-
-
-
-
- alignPoint |
- bool |
- false |
- Popup will align with mouse position (support action of 'click', 'hover' and 'contextMenu') |
-
-
- popupClassName |
- string |
- |
- additional className added to popup |
-
-
- forceRender |
- boolean |
- false |
- whether render popup before first show |
-
-
- destroyPopupOnHide |
- boolean |
- false |
- whether destroy popup when hide |
-
-
- getPopupClassNameFromAlign |
- getPopupClassNameFromAlign(align: Object):String |
- |
- additional className added to popup according to align |
-
-
- action |
- string[] |
- ['hover'] |
- which actions cause popup shown. enum of 'hover','click','focus','contextMenu' |
-
-
- mouseEnterDelay |
- number |
- 0 |
- delay time to show when mouse enter. unit: s. |
-
-
- mouseLeaveDelay |
- number |
- 0.1 |
- delay time to hide when mouse leave. unit: s. |
-
-
- popupStyle |
- Object |
- |
- additional style of popup |
-
-
- prefixCls |
- String |
- rc-trigger-popup |
- prefix class name |
-
-
- popupTransitionName |
- String|Object |
- |
- https://github.com/react-component/animate |
-
-
- maskTransitionName |
- String|Object |
- |
- https://github.com/react-component/animate |
-
-
- onPopupVisibleChange |
- Function |
- |
- call when popup visible is changed |
-
-
- mask |
- boolean |
- false |
- whether to support mask |
-
-
- maskClosable |
- boolean |
- true |
- whether to support click mask to hide |
-
-
- popupVisible |
- boolean |
- |
- whether popup is visible |
-
-
- zIndex |
- number |
- |
- popup's zIndex |
-
-
- defaultPopupVisible |
- boolean |
- |
- whether popup is visible initially |
-
-
- popupAlign |
- Object: alignConfig of [dom-align](https://github.com/yiminghe/dom-align) |
- |
- popup 's align config |
-
-
- onPopupAlign |
- function(popupDomNode, align) |
- |
- callback when popup node is aligned |
-
-
- popup |
- React.Element | function() => React.Element |
- |
- popup content |
-
-
- getPopupContainer |
- getPopupContainer(): HTMLElement |
- |
- function returning html node which will act as popup container |
-
-
- getDocument |
- getDocument(): HTMLElement |
- |
- function returning document node which will be attached click event to close trigger |
-
-
- popupPlacement |
- string |
- |
- use preset popup align config from builtinPlacements, can be merged by popupAlign prop |
-
-
- builtinPlacements |
- object |
- |
- builtin placement align map. used by placement prop |
-
-
- stretch |
- string |
- |
- Let popup div stretch with trigger element. enums of 'width', 'minWidth', 'height', 'minHeight'. (You can also mixed with 'height minWidth') |
-
-
-
+## API
+
+| Name | Type | Default | Description |
+| --- | --- | --- | --- |
## Test Case
@@ -255,4 +47,4 @@ open coverage/ dir
## License
-rc-trigger is released under the MIT license.
+rc-qrcode is released under the MIT license.
diff --git a/docs/demos/basic.md b/docs/demos/basic.md
new file mode 100644
index 0000000..824c331
--- /dev/null
+++ b/docs/demos/basic.md
@@ -0,0 +1,8 @@
+---
+title: Basic
+nav:
+ title: Demo
+ path: /demo
+---
+
+
\ No newline at end of file
diff --git a/docs/demos/body-overflow.md b/docs/demos/body-overflow.md
deleted file mode 100644
index 6f6f9c4..0000000
--- a/docs/demos/body-overflow.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Body Overflow
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/case.md b/docs/demos/case.md
deleted file mode 100644
index 053a1bc..0000000
--- a/docs/demos/case.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Case
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/click-nested.md b/docs/demos/click-nested.md
deleted file mode 100644
index c67236d..0000000
--- a/docs/demos/click-nested.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Click Nested
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/clip.md b/docs/demos/clip.md
deleted file mode 100644
index e2798ee..0000000
--- a/docs/demos/clip.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Clip
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/container.md b/docs/demos/container.md
deleted file mode 100644
index a4860cb..0000000
--- a/docs/demos/container.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Container
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/customStyle.md b/docs/demos/customStyle.md
new file mode 100644
index 0000000..c10fca2
--- /dev/null
+++ b/docs/demos/customStyle.md
@@ -0,0 +1,8 @@
+---
+title: Custom Style
+nav:
+ title: Demo
+ path: /demo
+---
+
+
\ No newline at end of file
diff --git a/docs/demos/download.md b/docs/demos/download.md
new file mode 100644
index 0000000..83ba153
--- /dev/null
+++ b/docs/demos/download.md
@@ -0,0 +1,8 @@
+---
+title: Download
+nav:
+ title: Demo
+ path: /demo
+---
+
+
\ No newline at end of file
diff --git a/docs/demos/inside.md b/docs/demos/inside.md
deleted file mode 100644
index a9853c3..0000000
--- a/docs/demos/inside.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Inside
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/large-popup.md b/docs/demos/large-popup.md
deleted file mode 100644
index 048c77a..0000000
--- a/docs/demos/large-popup.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Large Popup
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/level.md b/docs/demos/level.md
new file mode 100644
index 0000000..b669e67
--- /dev/null
+++ b/docs/demos/level.md
@@ -0,0 +1,8 @@
+---
+title: Level
+nav:
+ title: Demo
+ path: /demo
+---
+
+
\ No newline at end of file
diff --git a/docs/demos/nested.md b/docs/demos/nested.md
deleted file mode 100644
index 5daf43b..0000000
--- a/docs/demos/nested.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Nested
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/point.md b/docs/demos/point.md
deleted file mode 100644
index 073a89b..0000000
--- a/docs/demos/point.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Point
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/shadow.md b/docs/demos/shadow.md
deleted file mode 100644
index 379812b..0000000
--- a/docs/demos/shadow.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Shadow
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/simple.md b/docs/demos/simple.md
deleted file mode 100644
index dbb7c35..0000000
--- a/docs/demos/simple.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Simple
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/static-scroll.md b/docs/demos/static-scroll.md
deleted file mode 100644
index 14a1846..0000000
--- a/docs/demos/static-scroll.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Static Scroll
-nav:
- title: Demo
- path: /demo
----
-
-
diff --git a/docs/demos/visible-fallback.md b/docs/demos/visible-fallback.md
deleted file mode 100644
index cb5a8f2..0000000
--- a/docs/demos/visible-fallback.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: Visible Fallback
-nav:
- title: Demo
- path: /demo
----
-
-
\ No newline at end of file
diff --git a/docs/demos/withLogo.md b/docs/demos/withLogo.md
new file mode 100644
index 0000000..3d0acd3
--- /dev/null
+++ b/docs/demos/withLogo.md
@@ -0,0 +1,8 @@
+---
+title: WithLogo
+nav:
+ title: Demo
+ path: /demo
+---
+
+
\ No newline at end of file
diff --git a/docs/examples/basic.tsx b/docs/examples/basic.tsx
new file mode 100644
index 0000000..0783fdf
--- /dev/null
+++ b/docs/examples/basic.tsx
@@ -0,0 +1,21 @@
+import { QRCodeCanvas } from 'rc-qrcode';
+import React from 'react';
+
+export default () => {
+ const [value, setValue] = React.useState('https://ant-design.antgroup.com/');
+ return (
+
+ ) =>
+ setValue(e.target.value)
+ }
+ type="text"
+ placeholder="The value of qrcode"
+ style={{ width: '100%' }}
+ />
+
+
+
+ );
+};
diff --git a/docs/examples/body-overflow.tsx b/docs/examples/body-overflow.tsx
deleted file mode 100644
index 744e86a..0000000
--- a/docs/examples/body-overflow.tsx
+++ /dev/null
@@ -1,248 +0,0 @@
-/* eslint no-console:0 */
-import Trigger from 'rc-trigger';
-import React from 'react';
-import { createPortal } from 'react-dom';
-import '../../assets/index.less';
-
-const PortalDemo = () => {
- return createPortal(
-
- PortalNode
-
,
- document.body,
- );
-};
-
-export default () => {
- const [open, setOpen] = React.useState(false);
- const [open1, setOpen1] = React.useState(false);
- const [open2, setOpen2] = React.useState(false);
- const [open3, setOpen3] = React.useState(false);
- return (
-
-
-
- {
- console.log('Visible Change:', next);
- setOpen(next);
- }}
- popupTransitionName="rc-trigger-popup-zoom"
- popup={
-
-
-
-
-
- }
- // popupVisible
- popupStyle={{ boxShadow: '0 0 5px red' }}
- popupAlign={{
- points: ['tc', 'bc'],
- overflow: {
- shiftX: 50,
- adjustY: true,
- },
- htmlRegion: 'scroll',
- }}
- >
-
-
-
- {
- console.log('Visible Change:', next);
- setOpen1(next);
- }}
- popupTransitionName="rc-trigger-popup-zoom"
- popup={
-
-
-
- }
- // popupVisible
- popupStyle={{ boxShadow: '0 0 5px red' }}
- popupAlign={{
- points: ['tc', 'bc'],
- overflow: {
- shiftX: 50,
- adjustY: true,
- },
- htmlRegion: 'scroll',
- }}
- >
-
- Target Click
-
-
-
- {
- console.log('Visible Change:', next);
- setOpen2(next);
- }}
- popupTransitionName="rc-trigger-popup-zoom"
- popup={
-
- Target ContextMenu1
-
- }
- popupStyle={{ boxShadow: '0 0 5px red' }}
- popupAlign={{
- points: ['tc', 'bc'],
- overflow: {
- shiftX: 50,
- adjustY: true,
- },
- htmlRegion: 'scroll',
- }}
- >
-
- Target ContextMenu1
-
-
-
- {
- console.log('Visible Change:', next);
- setOpen3(next);
- }}
- popupTransitionName="rc-trigger-popup-zoom"
- popup={
-
- Target ContextMenu2
-
- }
- popupStyle={{ boxShadow: '0 0 5px red' }}
- popupAlign={{
- points: ['tc', 'bc'],
- overflow: {
- shiftX: 50,
- adjustY: true,
- },
- htmlRegion: 'scroll',
- }}
- >
-
- Target ContextMenu2
-
-
-
- );
-};
diff --git a/docs/examples/case.less b/docs/examples/case.less
deleted file mode 100644
index a1012d6..0000000
--- a/docs/examples/case.less
+++ /dev/null
@@ -1,109 +0,0 @@
-// .rc-trigger-popup-placement-right {
-// border-width: 10px!important;
-// }
-
-// ======================= Popup =======================
-.case-motion {
- transform-origin: 50% 50%;
-
- animation-duration: 0.3s;
- animation-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28);
- animation-fill-mode: both;
-
- &::after {
- content: 'Animating...';
- position: absolute;
- bottom: -3em;
- }
-
- &-appear,
- &-enter {
- animation-play-state: paused;
-
- &-active {
- animation-name: case-zoom-in;
- animation-play-state: running;
- }
- }
-
- &-leave {
- animation-play-state: paused;
-
- &-active {
- animation-name: case-zoom-out;
- animation-play-state: running;
- }
- }
-}
-
-@keyframes case-zoom-in {
- 0% {
- opacity: 0;
- transform: scale(0);
- }
- 100% {
- opacity: 1;
- transform: scale(1);
- }
-}
-
-@keyframes case-zoom-out {
- 0% {
- opacity: 1;
- transform: scale(1);
- }
- 100% {
- opacity: 0;
- transform: scale(1.2);
- }
-}
-
-// ======================= Mask =======================
-.mask-motion {
- animation-duration: 0.3s;
- animation-fill-mode: both;
- position: fixed;
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.3);
-
- &-appear,
- &-enter {
- animation-play-state: paused;
- opacity: 0;
-
- &-active {
- animation-name: mask-zoom-in;
- animation-play-state: running;
- }
- }
-
- &-leave {
- animation-play-state: paused;
-
- &-active {
- animation-name: mask-zoom-out;
- animation-play-state: running;
- }
- }
-}
-
-@keyframes mask-zoom-in {
- 0% {
- opacity: 0;
- }
- 100% {
- opacity: 1;
- }
-}
-
-@keyframes mask-zoom-out {
- 0% {
- opacity: 1;
- }
- 100% {
- opacity: 0;
- }
-}
diff --git a/docs/examples/case.tsx b/docs/examples/case.tsx
deleted file mode 100644
index 2d24a8e..0000000
--- a/docs/examples/case.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-/* eslint no-console:0 */
-
-import React from 'react';
-import type { CSSMotionProps } from 'rc-motion';
-import type { BuildInPlacements } from 'rc-trigger';
-import Trigger from 'rc-trigger';
-import './case.less';
-
-const builtinPlacements: BuildInPlacements = {
- left: {
- points: ['cr', 'cl'],
- },
- right: {
- points: ['cl', 'cr'],
- },
- top: {
- points: ['bc', 'tc'],
- },
- bottom: {
- points: ['tc', 'bc'],
- },
- topLeft: {
- points: ['bl', 'tl'],
- },
- topRight: {
- points: ['br', 'tr'],
- },
- bottomRight: {
- points: ['tr', 'br'],
- },
- bottomLeft: {
- points: ['tl', 'bl'],
- },
-};
-
-const Motion: CSSMotionProps = {
- motionName: 'case-motion',
-};
-
-const MaskMotion: CSSMotionProps = {
- motionName: 'mask-motion',
-};
-
-function useControl(valuePropName: string, defaultValue: T): [T, any] {
- const [value, setValue] = React.useState(defaultValue);
-
- return [
- value,
- {
- value,
- checked: value,
- onChange({ target }) {
- setValue(target[valuePropName]);
- },
- },
- ];
-}
-
-const LabelItem: React.FC<{
- title: React.ReactNode;
- children: React.ReactElement;
- [prop: string]: any;
-}> = ({ title, children, ...rest }) => {
- const { type } = children;
-
- const style = {
- display: 'inline-flex',
- padding: '0 8px',
- alignItems: 'center',
- };
-
- const spacing = ;
-
- if (type === 'input' && children.props.type === 'checkbox') {
- return (
-
- );
- }
-
- return (
-
- );
-};
-
-const Demo = () => {
- const [hover, hoverProps] = useControl('checked', true);
- const [focus, focusProps] = useControl('checked', false);
- const [click, clickProps] = useControl('checked', false);
- const [contextMenu, contextMenuProps] = useControl('checked', false);
-
- const [placement, placementProps] = useControl('value', 'right');
- const [stretch, stretchProps] = useControl('value', '');
- const [motion, motionProps] = useControl('checked', true);
- const [destroyPopupOnHide, destroyPopupOnHideProps] = useControl(
- 'checked',
- false,
- );
- const [mask, maskProps] = useControl('checked', false);
- const [maskClosable, maskClosableProps] = useControl('checked', true);
- const [forceRender, forceRenderProps] = useControl('checked', false);
- const [offsetX, offsetXProps] = useControl('value', 0);
- const [offsetY, offsetYProps] = useControl('value', 0);
-
- const actions = {
- hover,
- focus,
- click,
- contextMenu,
- };
-
- return (
-
-
-
- );
-};
-
-export default Demo;
diff --git a/docs/examples/click-nested.tsx b/docs/examples/click-nested.tsx
deleted file mode 100644
index 11c7e43..0000000
--- a/docs/examples/click-nested.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-/* eslint no-console:0 */
-
-import Trigger from 'rc-trigger';
-import React from 'react';
-import '../../assets/index.less';
-
-const builtinPlacements = {
- left: {
- points: ['cr', 'cl'],
- },
- right: {
- points: ['cl', 'cr'],
- },
- top: {
- points: ['bc', 'tc'],
- },
- bottom: {
- points: ['tc', 'bc'],
- },
- topLeft: {
- points: ['bl', 'tl'],
- },
- topRight: {
- points: ['br', 'tr'],
- },
- bottomRight: {
- points: ['tr', 'br'],
- },
- bottomLeft: {
- points: ['tl', 'bl'],
- },
-};
-
-const popupBorderStyle = {
- border: '1px solid red',
- padding: 10,
- background: 'rgba(255, 0, 0, 0.1)',
-};
-
-const NestPopup = ({ open, setOpen }) => {
- return (
- i am a click popup}
- popupVisible={open}
- onPopupVisibleChange={setOpen}
- >
-
- i am a click popup{' '}
-
-
-
- );
-};
-
-NestPopup.displayName = '🐞 NestPopup';
-
-const Test = () => {
- const [open1, setOpen1] = React.useState(false);
- const [open2, setOpen2] = React.useState(false);
-
- return (
-
-
-
- }
- fresh
- >
- Click Me
-
-
-
- );
-};
-
-export default Test;
diff --git a/docs/examples/clip.tsx b/docs/examples/clip.tsx
deleted file mode 100644
index 5f096b4..0000000
--- a/docs/examples/clip.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-/* eslint no-console:0 */
-import Trigger from 'rc-trigger';
-import React from 'react';
-import '../../assets/index.less';
-
-const builtinPlacements = {
- top: {
- points: ['bc', 'tc'],
- overflow: {
- adjustX: true,
- adjustY: true,
- },
- offset: [0, 0],
- },
- bottom: {
- points: ['tc', 'bc'],
- overflow: {
- adjustX: true,
- adjustY: true,
- },
- offset: [0, 0],
- },
-};
-
-const popupPlacement = 'top';
-
-export default () => {
- const [scale, setScale] = React.useState('1');
-
- return (
-
-
-
- setScale(e.target.value)}
- />
-
-
-
-
- Popup
-
- }
- getPopupContainer={(n) => n.parentNode as any}
- popupStyle={{ boxShadow: '0 0 5px red' }}
- popupPlacement={popupPlacement}
- builtinPlacements={builtinPlacements}
- stretch="minWidth"
- >
-
- Target
-
-
-
-
-
- {/* */}
-
- );
-};
diff --git a/docs/examples/container.tsx b/docs/examples/container.tsx
deleted file mode 100644
index 3a00aa0..0000000
--- a/docs/examples/container.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-/* eslint no-console:0 */
-import Trigger from 'rc-trigger';
-import React from 'react';
-import '../../assets/index.less';
-
-const builtinPlacements = {
- topLeft: {
- points: ['bl', 'tl'],
- overflow: {
- shiftX: 50,
- adjustY: true,
- },
- offset: [0, 0],
- targetOffset: [10, 0],
- },
- bottomLeft: {
- points: ['tl', 'bl'],
- overflow: {
- adjustX: true,
- adjustY: true,
- },
- },
- top: {
- points: ['bc', 'tc'],
- overflow: {
- shiftX: 50,
- adjustY: true,
- },
- offset: [0, -10],
- },
- bottom: {
- points: ['tc', 'bc'],
- overflow: {
- shiftX: true,
- adjustY: true,
- },
- offset: [0, 10],
- htmlRegion: 'scroll' as const,
- },
- left: {
- points: ['cr', 'cl'],
- overflow: {
- adjustX: true,
- shiftY: true,
- },
- offset: [-10, 0],
- },
- right: {
- points: ['cl', 'cr'],
- overflow: {
- adjustX: true,
- shiftY: 24,
- },
- offset: [10, 0],
- },
-};
-
-const popupPlacement = 'top';
-
-export default () => {
- console.log('Demo Render!');
-
- const [visible, setVisible] = React.useState(false);
- const [scale, setScale] = React.useState('1');
- const [targetVisible, setTargetVisible] = React.useState(true);
-
- const rootRef = React.useRef();
- const popHolderRef = React.useRef();
- const scrollRef = React.useRef();
-
- React.useEffect(() => {
- scrollRef.current.scrollLeft = window.innerWidth;
- scrollRef.current.scrollTop = window.innerHeight / 2;
- }, []);
-
- return (
-
-
-
- setScale(e.target.value)}
- />
-
-
-
-
-
-
-
- Popup
-
- }
- popupTransitionName="rc-trigger-popup-zoom"
- popupStyle={{ boxShadow: '0 0 5px red' }}
- popupVisible={visible}
- onPopupVisibleChange={(nextVisible) => {
- setVisible(nextVisible);
- }}
- // getPopupContainer={() => popHolderRef.current}
- popupPlacement={popupPlacement}
- builtinPlacements={builtinPlacements}
- stretch="minWidth"
- onPopupAlign={(domNode, align) => {
- console.log('onPopupAlign:', domNode, align);
- }}
- >
-
- Target
-
-
-
-
-
-
- {/* */}
-
- );
-};
diff --git a/docs/examples/customStyle.tsx b/docs/examples/customStyle.tsx
new file mode 100644
index 0000000..f19f7d4
--- /dev/null
+++ b/docs/examples/customStyle.tsx
@@ -0,0 +1,28 @@
+import { QRCodeCanvas } from 'rc-qrcode';
+import React from 'react';
+
+export default () => {
+ const [value, setValue] = React.useState('https://ant-design.antgroup.com/');
+ return (
+
+ ) =>
+ setValue(e.target.value)
+ }
+ type="text"
+ placeholder="The value of qrcode"
+ style={{ width: '100%' }}
+ />
+
+
+
+ );
+};
diff --git a/docs/examples/download.tsx b/docs/examples/download.tsx
new file mode 100644
index 0000000..ba7b037
--- /dev/null
+++ b/docs/examples/download.tsx
@@ -0,0 +1,45 @@
+import { QRCodeCanvas } from 'rc-qrcode';
+import React from 'react';
+
+const downloadQRCode = () => {
+ const canvas = document.getElementById('myqrcode')?.querySelector('canvas');
+ if (canvas) {
+ const url = canvas.toDataURL();
+ const a = document.createElement('a');
+ a.download = 'QRCode.png';
+ a.href = url;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ }
+};
+
+export default () => {
+ const [value, setValue] = React.useState('https://ant-design.antgroup.com/');
+ return (
+
+ ) =>
+ setValue(e.target.value)
+ }
+ type="text"
+ placeholder="The value of qrcode"
+ style={{ width: '80%' }}
+ />
+
+
+
+
+ );
+};
diff --git a/docs/examples/inside.tsx b/docs/examples/inside.tsx
deleted file mode 100644
index 42c19d4..0000000
--- a/docs/examples/inside.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-/* eslint no-console:0 */
-import React from 'react';
-import '../../assets/index.less';
-import Trigger, { type BuildInPlacements } from '../../src';
-
-const experimentalConfig = {
- _experimental: {
- dynamicInset: true,
- },
-};
-
-export const builtinPlacements: BuildInPlacements = {
- top: {
- points: ['bc', 'tc'],
- overflow: {
- shiftX: 0,
- adjustY: true,
- },
- offset: [0, 0],
- ...experimentalConfig,
- },
- topLeft: {
- points: ['bl', 'tl'],
- overflow: {
- adjustX: true,
- adjustY: false,
- shiftY: true,
- },
- offset: [0, -20],
- ...experimentalConfig,
- },
- topRight: {
- points: ['br', 'tr'],
- overflow: {
- adjustX: true,
- adjustY: true,
- },
- offset: [0, 0],
- ...experimentalConfig,
- },
- left: {
- points: ['cr', 'cl'],
- overflow: {
- adjustX: true,
- shiftY: true,
- },
- offset: [0, 0],
- ...experimentalConfig,
- },
- leftTop: {
- points: ['tr', 'tl'],
- overflow: {
- adjustX: true,
- adjustY: true,
- },
- offset: [0, 0],
- ...experimentalConfig,
- },
- leftBottom: {
- points: ['br', 'bl'],
- overflow: {
- adjustX: true,
- adjustY: true,
- },
- offset: [0, 0],
- ...experimentalConfig,
- },
- right: {
- points: ['cl', 'cr'],
- overflow: {
- adjustX: true,
- shiftY: true,
- },
- offset: [0, 0],
- ...experimentalConfig,
- },
- bottom: {
- points: ['tc', 'bc'],
- overflow: {
- shiftX: 50,
- adjustY: true,
- },
- offset: [0, 0],
- ...experimentalConfig,
- },
- bottomLeft: {
- points: ['tl', 'bl'],
- overflow: {
- shiftX: 50,
- adjustY: true,
- shiftY: true,
- },
- offset: [0, 20],
- ...experimentalConfig,
- },
-};
-
-const popupPlacement = 'bottomLeft';
-
-export default () => {
- const [popupHeight, setPopupHeight] = React.useState(60);
-
- const containerRef = React.useRef();
-
- React.useEffect(() => {
- containerRef.current.scrollLeft = document.defaultView.innerWidth;
- containerRef.current.scrollTop = document.defaultView.innerHeight;
- }, []);
-
- return (
- <>
-
-
-
-
-
-
- Popup
-
- }
- popupVisible
- getPopupContainer={() => containerRef.current}
- popupPlacement={popupPlacement}
- builtinPlacements={builtinPlacements}
- >
-
- Target
-
-
-
-
- >
- );
-};
diff --git a/docs/examples/large-popup.tsx b/docs/examples/large-popup.tsx
deleted file mode 100644
index 8d402d3..0000000
--- a/docs/examples/large-popup.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-/* eslint no-console:0 */
-import Trigger from 'rc-trigger';
-import React from 'react';
-import '../../assets/index.less';
-
-const builtinPlacements = {
- top: {
- points: ['bc', 'tc'],
- overflow: {
- shiftY: true,
- adjustY: true,
- },
- offset: [0, -10],
- },
- bottom: {
- points: ['tc', 'bc'],
- overflow: {
- shiftY: true,
- adjustY: true,
- },
- offset: [0, 10],
- htmlRegion: 'scroll' as const,
- },
-};
-
-export default () => {
- const containerRef = React.useRef();
-
- React.useEffect(() => {
- console.clear();
- containerRef.current.scrollTop = document.defaultView.innerHeight * 0.75;
- }, []);
-
- return (
-
-
-
-
-
- Popup 75vh
-
- }
- popupStyle={{ boxShadow: '0 0 5px red' }}
- popupVisible
- popupPlacement="top"
- builtinPlacements={builtinPlacements}
- >
-
- Target
-
-
-
-
-
-
- {/* */}
-
- );
-};
diff --git a/docs/examples/level.tsx b/docs/examples/level.tsx
new file mode 100644
index 0000000..67dc849
--- /dev/null
+++ b/docs/examples/level.tsx
@@ -0,0 +1,39 @@
+import type { ErrorCorrectionLevel } from 'rc-qrcode';
+import { QRCodeCanvas } from 'rc-qrcode';
+import React from 'react';
+
+export default () => {
+ const [value, setValue] = React.useState('https://ant-design.antgroup.com/');
+ const [level, setLevel] = React.useState('L');
+ return (
+
+ setLevel((e.target as HTMLInputElement).value)}>
+
+
+
+
+
+
+
+
+
+
+ ) =>
+ setValue(e.target.value)
+ }
+ type="text"
+ placeholder="The value of qrcode"
+ style={{ width: '80%' }}
+ />
+
+
+
+ );
+};
diff --git a/docs/examples/nested.tsx b/docs/examples/nested.tsx
deleted file mode 100644
index 3412438..0000000
--- a/docs/examples/nested.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-/* eslint no-console:0 */
-
-import React from 'react';
-import ReactDOM from 'react-dom';
-import Trigger from 'rc-trigger';
-import '../../assets/index.less';
-
-const builtinPlacements = {
- left: {
- points: ['cr', 'cl'],
- },
- right: {
- points: ['cl', 'cr'],
- },
- top: {
- points: ['bc', 'tc'],
- },
- bottom: {
- points: ['tc', 'bc'],
- },
- topLeft: {
- points: ['bl', 'tl'],
- },
- topRight: {
- points: ['br', 'tr'],
- },
- bottomRight: {
- points: ['tr', 'br'],
- },
- bottomLeft: {
- points: ['tl', 'bl'],
- },
-};
-
-const popupBorderStyle = {
- border: '1px solid red',
- padding: 10,
-};
-
-const OuterContent = ({ getContainer }) => {
- return ReactDOM.createPortal(
-
- I am outer content
-
-
,
- getContainer(),
- );
-};
-
-const Test = () => {
- const containerRef = React.useRef();
- const outerDivRef = React.useRef();
-
- const innerTrigger = (
-
-
-
containerRef.current}
- popup={I am inner Trigger Popup
}
- >
-
- clickToShowInnerTrigger
-
-
-
- );
- return (
-
-
-
- i am a click popup
- outerDivRef.current} />
-
- }
- >
-
- i am a hover popup }
- >
-
- trigger
-
-
-
-
-
-
-
-
- trigger
-
-
-
-
-
-
- );
-};
-
-export default Test;
diff --git a/docs/examples/point.less b/docs/examples/point.less
deleted file mode 100644
index 4adc341..0000000
--- a/docs/examples/point.less
+++ /dev/null
@@ -1,3 +0,0 @@
-.point-popup {
- pointer-events: none;
-}
\ No newline at end of file
diff --git a/docs/examples/point.tsx b/docs/examples/point.tsx
deleted file mode 100644
index 228fb18..0000000
--- a/docs/examples/point.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-/* eslint no-console:0 */
-
-import React from 'react';
-import Trigger from 'rc-trigger';
-import '../../assets/index.less';
-import './point.less';
-
-const builtinPlacements = {
- topLeft: {
- points: ['tl', 'tl'],
- },
-};
-
-const innerTrigger = (
- This is popup
-);
-
-class Test extends React.Component {
- state = {
- action: 'click',
- mouseEnterDelay: 0,
- };
-
- onActionChange = ({ target: { value } }) => {
- this.setState({ action: value });
- };
-
- onDelayChange = ({ target: { value } }) => {
- this.setState({ mouseEnterDelay: Number(value) || 0 });
- };
-
- render() {
- const { action, mouseEnterDelay } = this.state;
-
- return (
-
-
{' '}
- {action === 'hover' && (
-
- )}
-
-
-
- Interactive region
-
-
-
-
- );
- }
-}
-
-export default Test;
diff --git a/docs/examples/shadow.tsx b/docs/examples/shadow.tsx
deleted file mode 100644
index 6a1f5a2..0000000
--- a/docs/examples/shadow.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-/* eslint no-console:0 */
-import Trigger from 'rc-trigger';
-import React from 'react';
-import { createRoot } from 'react-dom/client';
-import '../../assets/index.less';
-
-const Demo = () => {
- return (
-
-
- Popup
-
- }
- popupStyle={{ boxShadow: '0 0 5px red', position: 'absolute' }}
- getPopupContainer={(item) => item.parentElement!}
- popupAlign={{
- points: ['bc', 'tc'],
- overflow: {
- shiftX: 50,
- adjustY: true,
- },
- offset: [0, -10],
- }}
- stretch="minWidth"
- autoDestroy
- >
-
- Target
-
-
-
- );
-};
-
-export default () => {
- React.useEffect(() => {
- const host = document.createElement('div');
- document.body.appendChild(host);
- host.style.background = 'rgba(255,0,0,0.1)';
- const shadowRoot = host.attachShadow({
- mode: 'open',
- delegatesFocus: false,
- });
- const container = document.createElement('div');
- shadowRoot.appendChild(container);
-
- createRoot(container).render();
- }, []);
-
- return null;
-};
diff --git a/docs/examples/simple.tsx b/docs/examples/simple.tsx
deleted file mode 100644
index 31b5388..0000000
--- a/docs/examples/simple.tsx
+++ /dev/null
@@ -1,409 +0,0 @@
-/* eslint no-console:0 */
-
-import Trigger from 'rc-trigger';
-import React from 'react';
-import '../../assets/index.less';
-
-const builtinPlacements = {
- left: {
- points: ['cr', 'cl'],
- offset: [-10, 0],
- },
- right: {
- points: ['cl', 'cr'],
- offset: [10, 0],
- },
- top: {
- points: ['bc', 'tc'],
- offset: [0, -10],
- },
- bottom: {
- points: ['tc', 'bc'],
- offset: [0, 10],
- },
- topLeft: {
- points: ['bl', 'tl'],
- offset: [0, -10],
- },
- topRight: {
- points: ['br', 'tr'],
- offset: [0, -10],
- },
- bottomRight: {
- points: ['tr', 'br'],
- offset: [0, 10],
- },
- bottomLeft: {
- points: ['tl', 'bl'],
- offset: [0, 10],
- },
-};
-
-function getPopupContainer(trigger) {
- return trigger.parentNode;
-}
-
-const InnerTarget = (props) => (
-
-
This is a example of trigger usage.
-
You can adjust the value above
-
which will also change the behaviour of popup.
-
-);
-
-const RefTarget = React.forwardRef((props, ref) => {
- React.useImperativeHandle(ref, () => ({}));
-
- return ;
-});
-
-interface TestState {
- mask: boolean;
- maskClosable: boolean;
- placement: string;
- trigger: {
- click?: boolean;
- focus?: boolean;
- hover?: boolean;
- contextMenu?: boolean;
- };
- offsetX: number;
- offsetY: number;
- stretch: string;
- transitionName: string;
- destroyed?: boolean;
- destroyPopupOnHide?: boolean;
- autoDestroy?: boolean;
- mobile?: boolean;
-}
-
-class Test extends React.Component {
- state: TestState = {
- mask: true,
- maskClosable: true,
- placement: 'bottom',
- trigger: {
- click: true,
- },
- offsetX: undefined,
- offsetY: undefined,
- stretch: 'minWidth',
- transitionName: 'rc-trigger-popup-zoom',
- };
-
- onPlacementChange = (e) => {
- this.setState({
- placement: e.target.value,
- });
- };
-
- onStretch = (e) => {
- this.setState({
- stretch: e.target.value,
- });
- };
-
- onTransitionChange = (e) => {
- this.setState({
- transitionName: e.target.checked ? e.target.value : '',
- });
- };
-
- onTriggerChange = ({ target: { checked, value } }) => {
- this.setState(({ trigger }) => {
- const clone = { ...trigger };
-
- if (checked) {
- clone[value] = 1;
- } else {
- delete clone[value];
- }
-
- return {
- trigger: clone,
- };
- });
- };
-
- onOffsetXChange = (e) => {
- const targetValue = e.target.value;
- this.setState({
- offsetX: targetValue || undefined,
- });
- };
-
- onOffsetYChange = (e) => {
- const targetValue = e.target.value;
- this.setState({
- offsetY: targetValue || undefined,
- });
- };
-
- onVisibleChange = (visible) => {
- console.log('tooltip', visible);
- };
-
- onMask = (e) => {
- this.setState({
- mask: e.target.checked,
- });
- };
-
- onMaskClosable = (e) => {
- this.setState({
- maskClosable: e.target.checked,
- });
- };
-
- getPopupAlign = () => {
- const { offsetX, offsetY } = this.state;
- return {
- offset: [offsetX, offsetY],
- overflow: {
- adjustX: 1,
- adjustY: 1,
- },
- };
- };
-
- destroy = () => {
- this.setState({
- destroyed: true,
- });
- };
-
- destroyPopupOnHide = (e) => {
- this.setState({
- destroyPopupOnHide: e.target.checked,
- });
- };
-
- autoDestroy = (e) => {
- this.setState({
- autoDestroy: e.target.checked,
- });
- };
-
- render() {
- const { state } = this;
- const { trigger } = state;
- if (state.destroyed) {
- return null;
- }
- return (
-
-
-
-
-
-
-
- trigger:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- i am a popup
}
- popupTransitionName={state.transitionName}
- mobile={
- state.mobile
- ? {
- popupMotion: {
- motionName: 'rc-trigger-popup-mobile-fade',
- },
- popupClassName: 'rc-trigger-popup-mobile',
- popupStyle: {
- padding: 16,
- borderTop: '1px solid red',
- background: '#FFF',
- textAlign: 'center',
- },
- popupRender: (node) => (
- <>
-
-
-
- {node}
- >
- ),
- }
- : null
- }
- >
-
-
-
-
- );
- }
-}
-
-export default Test;
diff --git a/docs/examples/static-scroll.tsx b/docs/examples/static-scroll.tsx
deleted file mode 100644
index 1f0e6e2..0000000
--- a/docs/examples/static-scroll.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint no-console:0 */
-import Trigger from 'rc-trigger';
-import React from 'react';
-import '../../assets/index.less';
-import { builtinPlacements } from './inside';
-
-export default () => {
- return (
-
-
-
- Popup
-
- }
- popupStyle={{ boxShadow: '0 0 5px red' }}
- popupVisible
- builtinPlacements={builtinPlacements}
- popupPlacement="top"
- stretch="minWidth"
- getPopupContainer={(e) => e.parentElement!}
- >
-
- Target
-
-
- {new Array(20).fill(null).map((_, index) => (
-
- Placeholder Line {index}
-
- ))}
-
-
- );
-};
diff --git a/docs/examples/visible-fallback.tsx b/docs/examples/visible-fallback.tsx
deleted file mode 100644
index 3e0881f..0000000
--- a/docs/examples/visible-fallback.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-/* eslint no-console:0 */
-import type { AlignType, TriggerRef } from 'rc-trigger';
-import Trigger from 'rc-trigger';
-import React from 'react';
-import '../../assets/index.less';
-
-const builtinPlacements: Record = {
- top: {
- points: ['bc', 'tc'],
- overflow: {
- adjustX: true,
- adjustY: true,
- },
- offset: [0, 0],
- htmlRegion: 'visibleFirst',
- },
- bottom: {
- points: ['tc', 'bc'],
- overflow: {
- adjustX: true,
- adjustY: true,
- },
- offset: [0, 0],
- htmlRegion: 'visibleFirst',
- },
-};
-
-export default () => {
- const [enoughTop, setEnoughTop] = React.useState(true);
-
- const triggerRef = React.useRef();
-
- React.useEffect(() => {
- triggerRef.current?.forceAlign();
- }, [enoughTop]);
-
- return (
-
- `visibleFirst` should not show in hidden region if still scrollable
-
-
-
-
-
- Should Always place bottom
-
- }
- getPopupContainer={(n) => n.parentNode as any}
- popupStyle={{ boxShadow: '0 0 5px red' }}
- popupPlacement={enoughTop ? 'bottom' : 'top'}
- builtinPlacements={builtinPlacements}
- stretch="minWidth"
- >
-
- Target
-
-
-
-
- );
-};
diff --git a/docs/examples/withLogo.tsx b/docs/examples/withLogo.tsx
new file mode 100644
index 0000000..d99d9e9
--- /dev/null
+++ b/docs/examples/withLogo.tsx
@@ -0,0 +1,30 @@
+import { QRCodeCanvas } from 'rc-qrcode';
+import React from 'react';
+
+export default () => {
+ const [value, setValue] = React.useState('https://ant-design.antgroup.com/');
+ return (
+
+ ) =>
+ setValue(e.target.value)
+ }
+ type="text"
+ placeholder="The value of qrcode"
+ style={{ width: '100%' }}
+ />
+
+
+
+ );
+};
diff --git a/docs/index.md b/docs/index.md
index 2782d1c..0f134bd 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,7 +1,7 @@
---
hero:
- title: rc-trigger
- description: React Trigger Component
+ title: rc-qrcode
+ description: React Qrcode Component
---
\ No newline at end of file
diff --git a/index.js b/index.js
deleted file mode 100644
index 274f820..0000000
--- a/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// export this package's api
-import Trigger from './src/';
-export default Trigger;
diff --git a/index.ts b/index.ts
new file mode 100644
index 0000000..fcd952d
--- /dev/null
+++ b/index.ts
@@ -0,0 +1 @@
+export * from "./src";
\ No newline at end of file
diff --git a/jest.config.js b/jest.config.js
index 5328c18..1eb8160 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,3 +1,11 @@
module.exports = {
- setupFiles: ['./tests/setup.js'],
+ testEnvironment: 'jsdom',
+ setupFiles: ['./tests/setup.ts', 'jest-canvas-mock'],
+ setupFilesAfterEnv: ['/tests/setupAfterEnv.ts'],
+ verbose: true,
+ globals: {
+ 'ts-jest': {
+ tsConfig: './tsconfig.json',
+ },
+ },
};
diff --git a/now.json b/now.json
index 76d28fa..7e4bf1e 100644
--- a/now.json
+++ b/now.json
@@ -1,11 +1,11 @@
{
"version": 2,
- "name": "rc-trigger",
+ "name": "rc-qrcode",
"builds": [
{
"src": "package.json",
"use": "@now/static-build",
- "config": { "distDir": ".doc" }
+ "config": { "distDir": "dist" }
}
]
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 7d14086..fbe5986 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
- "name": "@rc-component/trigger",
- "version": "2.2.0",
+ "name": "@rc-component/qrcode",
+ "version": "1.0.0",
"description": "base abstract trigger component for react",
"engines": {
"node": ">=8.x"
@@ -8,17 +8,17 @@
"keywords": [
"react",
"react-component",
- "react-trigger",
- "trigger"
+ "react-qrcode",
+ "qrcode"
],
- "homepage": "https://github.com/react-component/trigger",
+ "homepage": "https://github.com/react-component/qrcode",
"author": "",
"repository": {
"type": "git",
- "url": "https://github.com/react-component/trigger.git"
+ "url": "https://github.com/react-component/qrcode.git"
},
"bugs": {
- "url": "https://github.com/react-component/trigger/issues"
+ "url": "https://github.com/react-component/qrcode/issues"
},
"files": [
"es",
@@ -35,11 +35,12 @@
"compile": "father build && lessc assets/index.less assets/index.css",
"prepublishOnly": "npm run compile && np --yolo --no-publish",
"lint": "eslint src/ docs/examples/ --ext .tsx,.ts,.jsx,.js",
- "test": "rc-test",
+ "test": "umi-test",
"coverage": "rc-test --coverage",
"now-build": "npm run build"
},
"devDependencies": {
+ "@ant-design/tools": "^18.0.2",
"@rc-component/father-plugin": "^1.0.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^15.0.4",
@@ -53,20 +54,20 @@
"dumi": "^2.1.0",
"eslint": "^8.51.0",
"father": "^4.0.0",
+ "jest-canvas-mock": "^2.5.2",
"less": "^4.2.0",
"np": "^10.0.5",
"rc-test": "^7.0.13",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"regenerator-runtime": "^0.14.0",
- "typescript": "^5.1.6"
+ "ts-jest": "^29.1.4",
+ "typescript": "^5.1.6",
+ "umi-test": "^1.9.7"
},
"dependencies": {
- "@babel/runtime": "^7.23.2",
- "@rc-component/portal": "^1.1.0",
+ "@babel/runtime": "^7.24.7",
"classnames": "^2.3.2",
- "rc-motion": "^2.0.0",
- "rc-resize-observer": "^1.3.1",
"rc-util": "^5.38.0"
},
"peerDependencies": {
diff --git a/src/Popup/Arrow.tsx b/src/Popup/Arrow.tsx
deleted file mode 100644
index c61c49e..0000000
--- a/src/Popup/Arrow.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import classNames from 'classnames';
-import * as React from 'react';
-import type { AlignType, ArrowPos, ArrowTypeOuter } from '../interface';
-
-export interface ArrowProps {
- prefixCls: string;
- align: AlignType;
- arrow: ArrowTypeOuter;
- arrowPos: ArrowPos;
-}
-
-export default function Arrow(props: ArrowProps) {
- const { prefixCls, align, arrow, arrowPos } = props;
-
- const { className, content } = arrow || {};
- const { x = 0, y = 0 } = arrowPos;
-
- const arrowRef = React.useRef();
-
- // Skip if no align
- if (!align || !align.points) {
- return null;
- }
-
- const alignStyle: React.CSSProperties = {
- position: 'absolute',
- };
-
- // Skip if no need to align
- if (align.autoArrow !== false) {
- const popupPoints = align.points[0];
- const targetPoints = align.points[1];
- const popupTB = popupPoints[0];
- const popupLR = popupPoints[1];
- const targetTB = targetPoints[0];
- const targetLR = targetPoints[1];
-
- // Top & Bottom
- if (popupTB === targetTB || !['t', 'b'].includes(popupTB)) {
- alignStyle.top = y;
- } else if (popupTB === 't') {
- alignStyle.top = 0;
- } else {
- alignStyle.bottom = 0;
- }
-
- // Left & Right
- if (popupLR === targetLR || !['l', 'r'].includes(popupLR)) {
- alignStyle.left = x;
- } else if (popupLR === 'l') {
- alignStyle.left = 0;
- } else {
- alignStyle.right = 0;
- }
- }
-
- return (
-
- {content}
-
- );
-}
diff --git a/src/Popup/Mask.tsx b/src/Popup/Mask.tsx
deleted file mode 100644
index 86429ee..0000000
--- a/src/Popup/Mask.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import classNames from 'classnames';
-import type { CSSMotionProps } from 'rc-motion';
-import CSSMotion from 'rc-motion';
-import * as React from 'react';
-
-export interface MaskProps {
- prefixCls: string;
- open?: boolean;
- zIndex?: number;
- mask?: boolean;
-
- // Motion
- motion?: CSSMotionProps;
-}
-
-export default function Mask(props: MaskProps) {
- const {
- prefixCls,
- open,
- zIndex,
-
- mask,
- motion,
- } = props;
-
- if (!mask) {
- return null;
- }
-
- return (
-
- {({ className }) => (
-
- )}
-
- );
-}
diff --git a/src/Popup/PopupContent.tsx b/src/Popup/PopupContent.tsx
deleted file mode 100644
index eb96dff..0000000
--- a/src/Popup/PopupContent.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as React from 'react';
-
-export interface PopupContentProps {
- children?: React.ReactNode;
- cache?: boolean;
-}
-
-const PopupContent = React.memo(
- ({ children }: PopupContentProps) => children as React.ReactElement,
- (_, next) => next.cache,
-);
-
-if (process.env.NODE_ENV !== 'production') {
- PopupContent.displayName = 'PopupContent';
-}
-
-export default PopupContent;
diff --git a/src/Popup/index.tsx b/src/Popup/index.tsx
deleted file mode 100644
index e5d64d4..0000000
--- a/src/Popup/index.tsx
+++ /dev/null
@@ -1,285 +0,0 @@
-import classNames from 'classnames';
-import type { CSSMotionProps } from 'rc-motion';
-import CSSMotion from 'rc-motion';
-import ResizeObserver from 'rc-resize-observer';
-import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
-import { composeRef } from 'rc-util/lib/ref';
-import * as React from 'react';
-import type { TriggerProps } from '../';
-import type { AlignType, ArrowPos, ArrowTypeOuter } from '../interface';
-import Arrow from './Arrow';
-import Mask from './Mask';
-import PopupContent from './PopupContent';
-
-export interface PopupProps {
- prefixCls: string;
- className?: string;
- style?: React.CSSProperties;
- popup?: TriggerProps['popup'];
- target: HTMLElement;
- onMouseEnter?: React.MouseEventHandler;
- onMouseLeave?: React.MouseEventHandler;
- onPointerEnter?: React.MouseEventHandler;
- zIndex?: number;
-
- mask?: boolean;
- onVisibleChanged: (visible: boolean) => void;
-
- // Arrow
- align?: AlignType;
- arrow?: ArrowTypeOuter;
- arrowPos: ArrowPos;
-
- // Open
- open: boolean;
- /** Tell Portal that should keep in screen. e.g. should wait all motion end */
- keepDom: boolean;
- fresh?: boolean;
-
- // Click
- onClick?: React.MouseEventHandler;
-
- // Motion
- motion?: CSSMotionProps;
- maskMotion?: CSSMotionProps;
-
- // Portal
- forceRender?: boolean;
- getPopupContainer?: TriggerProps['getPopupContainer'];
- autoDestroy?: boolean;
- portal: React.ComponentType;
-
- // Align
- ready: boolean;
- offsetX: number;
- offsetY: number;
- offsetR: number;
- offsetB: number;
- onAlign: VoidFunction;
- onPrepare: () => Promise;
-
- // stretch
- stretch?: string;
- targetWidth?: number;
- targetHeight?: number;
-}
-
-const Popup = React.forwardRef((props, ref) => {
- const {
- popup,
- className,
- prefixCls,
- style,
- target,
-
- onVisibleChanged,
-
- // Open
- open,
- keepDom,
- fresh,
-
- // Click
- onClick,
-
- // Mask
- mask,
-
- // Arrow
- arrow,
- arrowPos,
- align,
-
- // Motion
- motion,
- maskMotion,
-
- // Portal
- forceRender,
- getPopupContainer,
- autoDestroy,
- portal: Portal,
-
- zIndex,
-
- onMouseEnter,
- onMouseLeave,
- onPointerEnter,
-
- ready,
- offsetX,
- offsetY,
- offsetR,
- offsetB,
- onAlign,
- onPrepare,
-
- stretch,
- targetWidth,
- targetHeight,
- } = props;
-
- const childNode = typeof popup === 'function' ? popup() : popup;
-
- // We can not remove holder only when motion finished.
- const isNodeVisible = open || keepDom;
-
- // ======================= Container ========================
- const getPopupContainerNeedParams = getPopupContainer?.length > 0;
-
- const [show, setShow] = React.useState(
- !getPopupContainer || !getPopupContainerNeedParams,
- );
-
- // Delay to show since `getPopupContainer` need target element
- useLayoutEffect(() => {
- if (!show && getPopupContainerNeedParams && target) {
- setShow(true);
- }
- }, [show, getPopupContainerNeedParams, target]);
-
- // ========================= Render =========================
- if (!show) {
- return null;
- }
-
- // >>>>> Offset
- const AUTO = 'auto' as const;
-
- const offsetStyle: React.CSSProperties = {
- left: '-1000vw',
- top: '-1000vh',
- right: AUTO,
- bottom: AUTO,
- };
-
- // Set align style
- if (ready || !open) {
- const { points } = align;
- const dynamicInset =
- align.dynamicInset || (align as any)._experimental?.dynamicInset;
- const alignRight = dynamicInset && points[0][1] === 'r';
- const alignBottom = dynamicInset && points[0][0] === 'b';
-
- if (alignRight) {
- offsetStyle.right = offsetR;
- offsetStyle.left = AUTO;
- } else {
- offsetStyle.left = offsetX;
- offsetStyle.right = AUTO;
- }
-
- if (alignBottom) {
- offsetStyle.bottom = offsetB;
- offsetStyle.top = AUTO;
- } else {
- offsetStyle.top = offsetY;
- offsetStyle.bottom = AUTO;
- }
- }
-
- // >>>>> Misc
- const miscStyle: React.CSSProperties = {};
- if (stretch) {
- if (stretch.includes('height') && targetHeight) {
- miscStyle.height = targetHeight;
- } else if (stretch.includes('minHeight') && targetHeight) {
- miscStyle.minHeight = targetHeight;
- }
- if (stretch.includes('width') && targetWidth) {
- miscStyle.width = targetWidth;
- } else if (stretch.includes('minWidth') && targetWidth) {
- miscStyle.minWidth = targetWidth;
- }
- }
-
- if (!open) {
- miscStyle.pointerEvents = 'none';
- }
-
- return (
- getPopupContainer(target))}
- autoDestroy={autoDestroy}
- >
-
-
- {(resizeObserverRef) => {
- return (
- {
- motion?.onVisibleChanged?.(nextVisible);
- onVisibleChanged(nextVisible);
- }}
- >
- {(
- { className: motionClassName, style: motionStyle },
- motionRef,
- ) => {
- const cls = classNames(prefixCls, motionClassName, className);
-
- return (
-
- {arrow && (
-
- )}
-
- {childNode}
-
-
- );
- }}
-
- );
- }}
-
-
- );
-});
-
-if (process.env.NODE_ENV !== 'production') {
- Popup.displayName = 'Popup';
-}
-
-export default Popup;
diff --git a/src/QRCodeCanvas.tsx b/src/QRCodeCanvas.tsx
new file mode 100644
index 0000000..c66ab90
--- /dev/null
+++ b/src/QRCodeCanvas.tsx
@@ -0,0 +1,175 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { useQRCode } from './hooks/useQRCode';
+import type { QRPropsCanvas } from './interface';
+import {
+ DEFAULT_BGCOLOR,
+ DEFAULT_FGCOLOR,
+ DEFAULT_INCLUDEMARGIN,
+ DEFAULT_LEVEL,
+ DEFAULT_MINVERSION,
+ DEFAULT_SIZE,
+ SUPPORTS_PATH2D,
+ excavateModules,
+ generatePath,
+} from './utils';
+
+const QRCodeCanvas = React.forwardRef(
+ function QRCodeCanvas(props, forwardedRef) {
+ const {
+ value,
+ size = DEFAULT_SIZE,
+ level = DEFAULT_LEVEL,
+ bgColor = DEFAULT_BGCOLOR,
+ fgColor = DEFAULT_FGCOLOR,
+ includeMargin = DEFAULT_INCLUDEMARGIN,
+ minVersion = DEFAULT_MINVERSION,
+ marginSize,
+ style,
+ imageSettings,
+ ...otherProps
+ } = props;
+ const imgSrc = imageSettings?.src;
+ const _canvas = useRef(null);
+ const _image = useRef(null);
+
+ // Set the local ref (_canvas) and also the forwarded ref from outside
+ const setCanvasRef = useCallback(
+ (node: HTMLCanvasElement | null) => {
+ _canvas.current = node;
+ if (typeof forwardedRef === 'function') {
+ forwardedRef(node);
+ } else if (forwardedRef) {
+ forwardedRef.current = node;
+ }
+ },
+ [forwardedRef],
+ );
+
+ // We're just using this state to trigger rerenders when images load. We
+ // Don't actually read the value anywhere. A smarter use of useEffect would
+ // depend on this value.
+ const [, setIsImageLoaded] = useState(false);
+
+ const { margin, cells, numCells, calculatedImageSettings } = useQRCode({
+ value,
+ level,
+ minVersion,
+ includeMargin,
+ marginSize,
+ imageSettings,
+ size,
+ });
+
+ useEffect(() => {
+ // Always update the canvas. It's cheap enough and we want to be correct
+ // with the current state.
+ if (_canvas.current != null) {
+ const canvas = _canvas.current;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ return;
+ }
+
+ let cellsToDraw = cells;
+ const image = _image.current;
+ const haveImageToRender =
+ calculatedImageSettings != null &&
+ image !== null &&
+ image.complete &&
+ image.naturalHeight !== 0 &&
+ image.naturalWidth !== 0;
+
+ if (haveImageToRender) {
+ if (calculatedImageSettings.excavation != null) {
+ cellsToDraw = excavateModules(
+ cells,
+ calculatedImageSettings.excavation,
+ );
+ }
+ }
+
+ // We're going to scale this so that the number of drawable units
+ // matches the number of cells. This avoids rounding issues, but does
+ // result in some potentially unwanted single pixel issues between
+ // blocks, only in environments that don't support Path2D.
+ const pixelRatio = window.devicePixelRatio || 1;
+ canvas.height = canvas.width = size * pixelRatio;
+ const scale = (size / numCells) * pixelRatio;
+ ctx.scale(scale, scale);
+ // console.log('scale', scale, 'size', size, 'numCells', numCells, 'pixelRatio', pixelRatio);
+
+ // Draw solid background, only paint dark modules.
+ ctx.fillStyle = bgColor;
+ ctx.fillRect(0, 0, numCells, numCells);
+
+ ctx.fillStyle = fgColor;
+ if (SUPPORTS_PATH2D) {
+ // $FlowFixMe: Path2D c'tor doesn't support args yet.
+ ctx.fill(new Path2D(generatePath(cellsToDraw, margin)));
+ } else {
+ cells.forEach(function (row, rdx) {
+ row.forEach(function (cell, cdx) {
+ if (cell) {
+ ctx.fillRect(cdx + margin, rdx + margin, 1, 1);
+ }
+ });
+ });
+ }
+
+ if (calculatedImageSettings) {
+ ctx.globalAlpha = calculatedImageSettings.opacity;
+ }
+
+ if (haveImageToRender) {
+ ctx.drawImage(
+ image,
+ calculatedImageSettings.x + margin,
+ calculatedImageSettings.y + margin,
+ calculatedImageSettings.w,
+ calculatedImageSettings.h,
+ );
+ }
+ }
+ });
+
+ // Ensure we mark image loaded as false here so we trigger updating the
+ // canvas in our other effect.
+ useEffect(() => {
+ setIsImageLoaded(false);
+ }, [imgSrc]);
+
+ const canvasStyle = { height: size, width: size, ...style };
+ let img = null;
+ if (imgSrc != null) {
+ img = (
+ {
+ setIsImageLoaded(true);
+ }}
+ ref={_image}
+ crossOrigin={calculatedImageSettings?.crossOrigin}
+ />
+ );
+ }
+ return (
+ <>
+
+ {img}
+ >
+ );
+ },
+);
+QRCodeCanvas.displayName = 'QRCodeCanvas';
+
+export { QRCodeCanvas };
diff --git a/src/QRCodeSVG.tsx b/src/QRCodeSVG.tsx
new file mode 100644
index 0000000..d605fb8
--- /dev/null
+++ b/src/QRCodeSVG.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import type { QRPropsSVG } from './interface';
+import {
+ DEFAULT_BGCOLOR,
+ DEFAULT_FGCOLOR,
+ DEFAULT_INCLUDEMARGIN,
+ DEFAULT_LEVEL,
+ DEFAULT_MINVERSION,
+ DEFAULT_SIZE,
+ excavateModules,
+ generatePath,
+} from './utils';
+import { useQRCode } from './hooks/useQRCode';
+
+const QRCodeSVG = React.forwardRef(
+ function QRCodeSVG(props, forwardedRef) {
+ const {
+ value,
+ size = DEFAULT_SIZE,
+ level = DEFAULT_LEVEL,
+ bgColor = DEFAULT_BGCOLOR,
+ fgColor = DEFAULT_FGCOLOR,
+ includeMargin = DEFAULT_INCLUDEMARGIN,
+ minVersion = DEFAULT_MINVERSION,
+ title,
+ marginSize,
+ imageSettings,
+ ...otherProps
+ } = props;
+
+ const { margin, cells, numCells, calculatedImageSettings } = useQRCode({
+ value,
+ level,
+ minVersion,
+ includeMargin,
+ marginSize,
+ imageSettings,
+ size,
+ });
+
+ let cellsToDraw = cells;
+ let image = null;
+ if (imageSettings != null && calculatedImageSettings != null) {
+ if (calculatedImageSettings.excavation != null) {
+ cellsToDraw = excavateModules(
+ cells,
+ calculatedImageSettings.excavation,
+ );
+ }
+
+ image = (
+
+ );
+ }
+
+ // Drawing strategy: instead of a rect per module, we're going to create a
+ // single path for the dark modules and layer that on top of a light rect,
+ // for a total of 2 DOM nodes. We pay a bit more in string concat but that's
+ // way faster than DOM ops.
+ // For level 1, 441 nodes -> 2
+ // For level 40, 31329 -> 2
+ const fgPath = generatePath(cellsToDraw, margin);
+
+ return (
+
+ );
+ },
+);
+QRCodeSVG.displayName = 'QRCodeSVG';
+
+export { QRCodeSVG };
diff --git a/src/TriggerWrapper.tsx b/src/TriggerWrapper.tsx
deleted file mode 100644
index 48d27cf..0000000
--- a/src/TriggerWrapper.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { fillRef, supportRef, useComposeRef } from 'rc-util/lib/ref';
-import * as React from 'react';
-import type { TriggerProps } from '.';
-
-export interface TriggerWrapperProps {
- getTriggerDOMNode?: TriggerProps['getTriggerDOMNode'];
- children: React.ReactElement;
-}
-
-const TriggerWrapper = React.forwardRef(
- (props, ref) => {
- const { children, getTriggerDOMNode } = props;
-
- const canUseRef = supportRef(children);
-
- // When use `getTriggerDOMNode`, we should do additional work to get the real dom
- const setRef = React.useCallback(
- (node) => {
- fillRef(ref, getTriggerDOMNode ? getTriggerDOMNode(node) : node);
- },
- [getTriggerDOMNode],
- );
-
- const mergedRef = useComposeRef(setRef, (children as any).ref);
-
- return canUseRef
- ? React.cloneElement(children, {
- ref: mergedRef,
- })
- : children;
- },
-);
-
-if (process.env.NODE_ENV !== 'production') {
- TriggerWrapper.displayName = 'TriggerWrapper';
-}
-
-export default TriggerWrapper;
diff --git a/src/context.ts b/src/context.ts
deleted file mode 100644
index 429b350..0000000
--- a/src/context.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as React from 'react';
-
-export interface TriggerContextProps {
- registerSubPopup: (id: string, node: HTMLElement) => void;
-}
-
-const TriggerContext = React.createContext(null);
-
-export default TriggerContext;
diff --git a/src/hooks/useAction.ts b/src/hooks/useAction.ts
deleted file mode 100644
index 99d78bb..0000000
--- a/src/hooks/useAction.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import * as React from 'react';
-import type { ActionType } from '../interface';
-
-type ActionTypes = ActionType | ActionType[];
-
-function toArray(val?: T | T[]) {
- return val ? (Array.isArray(val) ? val : [val]) : [];
-}
-
-export default function useAction(
- mobile: boolean,
- action: ActionTypes,
- showAction?: ActionTypes,
- hideAction?: ActionTypes,
-): [showAction: Set, hideAction: Set] {
- return React.useMemo(() => {
- const mergedShowAction = toArray(showAction ?? action);
- const mergedHideAction = toArray(hideAction ?? action);
-
- const showActionSet = new Set(mergedShowAction);
- const hideActionSet = new Set(mergedHideAction);
-
- if (mobile) {
- if (showActionSet.has('hover')) {
- showActionSet.delete('hover');
- showActionSet.add('click');
- }
-
- if (hideActionSet.has('hover')) {
- hideActionSet.delete('hover');
- hideActionSet.add('click');
- }
- }
-
- return [showActionSet, hideActionSet];
- }, [mobile, action, showAction, hideAction]);
-}
diff --git a/src/hooks/useAlign.ts b/src/hooks/useAlign.ts
deleted file mode 100644
index 6c15a09..0000000
--- a/src/hooks/useAlign.ts
+++ /dev/null
@@ -1,746 +0,0 @@
-import { isDOM } from 'rc-util/lib/Dom/findDOMNode';
-import isVisible from 'rc-util/lib/Dom/isVisible';
-import useEvent from 'rc-util/lib/hooks/useEvent';
-import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
-import * as React from 'react';
-import type { TriggerProps } from '..';
-import type {
- AlignPointLeftRight,
- AlignPointTopBottom,
- AlignType,
- OffsetType,
-} from '../interface';
-import { collectScroller, getVisibleArea, getWin, toNum } from '../util';
-
-type Rect = Record<'x' | 'y' | 'width' | 'height', number>;
-
-type Points = [topBottom: AlignPointTopBottom, leftRight: AlignPointLeftRight];
-
-function getUnitOffset(size: number, offset: OffsetType = 0) {
- const offsetStr = `${offset}`;
- const cells = offsetStr.match(/^(.*)\%$/);
- if (cells) {
- return size * (parseFloat(cells[1]) / 100);
- }
- return parseFloat(offsetStr);
-}
-
-function getNumberOffset(
- rect: { width: number; height: number },
- offset?: OffsetType[],
-) {
- const [offsetX, offsetY] = offset || [];
-
- return [
- getUnitOffset(rect.width, offsetX),
- getUnitOffset(rect.height, offsetY),
- ];
-}
-
-function splitPoints(points: string = ''): Points {
- return [points[0] as any, points[1] as any];
-}
-
-function getAlignPoint(rect: Rect, points: Points) {
- const topBottom = points[0];
- const leftRight = points[1];
-
- let x: number;
- let y: number;
-
- // Top & Bottom
- if (topBottom === 't') {
- y = rect.y;
- } else if (topBottom === 'b') {
- y = rect.y + rect.height;
- } else {
- y = rect.y + rect.height / 2;
- }
-
- // Left & Right
- if (leftRight === 'l') {
- x = rect.x;
- } else if (leftRight === 'r') {
- x = rect.x + rect.width;
- } else {
- x = rect.x + rect.width / 2;
- }
-
- return { x, y };
-}
-
-function reversePoints(points: Points, index: number): string {
- const reverseMap = {
- t: 'b',
- b: 't',
- l: 'r',
- r: 'l',
- };
-
- return points
- .map((point, i) => {
- if (i === index) {
- return reverseMap[point] || 'c';
- }
- return point;
- })
- .join('');
-}
-
-export default function useAlign(
- open: boolean,
- popupEle: HTMLElement,
- target: HTMLElement | [x: number, y: number],
- placement: string,
- builtinPlacements: any,
- popupAlign?: AlignType,
- onPopupAlign?: TriggerProps['onPopupAlign'],
-): [
- ready: boolean,
- offsetX: number,
- offsetY: number,
- offsetR: number,
- offsetB: number,
- arrowX: number,
- arrowY: number,
- scaleX: number,
- scaleY: number,
- align: AlignType,
- onAlign: VoidFunction,
-] {
- const [offsetInfo, setOffsetInfo] = React.useState<{
- ready: boolean;
- offsetX: number;
- offsetY: number;
- offsetR: number;
- offsetB: number;
- arrowX: number;
- arrowY: number;
- scaleX: number;
- scaleY: number;
- align: AlignType;
- }>({
- ready: false,
- offsetX: 0,
- offsetY: 0,
- offsetR: 0,
- offsetB: 0,
- arrowX: 0,
- arrowY: 0,
- scaleX: 1,
- scaleY: 1,
- align: builtinPlacements[placement] || {},
- });
- const alignCountRef = React.useRef(0);
-
- const scrollerList = React.useMemo(() => {
- if (!popupEle) {
- return [];
- }
-
- return collectScroller(popupEle);
- }, [popupEle]);
-
- // ========================= Flip ==========================
- // We will memo flip info.
- // If size change to make flip, it will memo the flip info and use it in next align.
- const prevFlipRef = React.useRef<{
- tb?: boolean;
- bt?: boolean;
- lr?: boolean;
- rl?: boolean;
- }>({});
-
- const resetFlipCache = () => {
- prevFlipRef.current = {};
- };
-
- if (!open) {
- resetFlipCache();
- }
-
- // ========================= Align =========================
- const onAlign = useEvent(() => {
- if (popupEle && target && open) {
- const popupElement = popupEle;
-
- const doc = popupElement.ownerDocument;
- const win = getWin(popupElement);
-
- const {
- width,
- height,
- position: popupPosition,
- } = win.getComputedStyle(popupElement);
-
- const originLeft = popupElement.style.left;
- const originTop = popupElement.style.top;
- const originRight = popupElement.style.right;
- const originBottom = popupElement.style.bottom;
- const originOverflow = popupElement.style.overflow;
-
- // Placement
- const placementInfo: AlignType = {
- ...builtinPlacements[placement],
- ...popupAlign,
- };
-
- // placeholder element
- const placeholderElement = doc.createElement('div');
- popupElement.parentElement?.appendChild(placeholderElement);
- placeholderElement.style.left = `${popupElement.offsetLeft}px`;
- placeholderElement.style.top = `${popupElement.offsetTop}px`;
- placeholderElement.style.position = popupPosition;
- placeholderElement.style.height = `${popupElement.offsetHeight}px`;
- placeholderElement.style.width = `${popupElement.offsetWidth}px`;
-
- // Reset first
- popupElement.style.left = '0';
- popupElement.style.top = '0';
- popupElement.style.right = 'auto';
- popupElement.style.bottom = 'auto';
- popupElement.style.overflow = 'hidden';
-
- // Calculate align style, we should consider `transform` case
- let targetRect: Rect;
- if (Array.isArray(target)) {
- targetRect = {
- x: target[0],
- y: target[1],
- width: 0,
- height: 0,
- };
- } else {
- const rect = target.getBoundingClientRect();
- targetRect = {
- x: rect.x,
- y: rect.y,
- width: rect.width,
- height: rect.height,
- };
- }
- const popupRect = popupElement.getBoundingClientRect();
- const {
- clientWidth,
- clientHeight,
- scrollWidth,
- scrollHeight,
- scrollTop,
- scrollLeft,
- } = doc.documentElement;
-
- const popupHeight = popupRect.height;
- const popupWidth = popupRect.width;
-
- const targetHeight = targetRect.height;
- const targetWidth = targetRect.width;
-
- // Get bounding of visible area
- const visibleRegion = {
- left: 0,
- top: 0,
- right: clientWidth,
- bottom: clientHeight,
- };
-
- const scrollRegion = {
- left: -scrollLeft,
- top: -scrollTop,
- right: scrollWidth - scrollLeft,
- bottom: scrollHeight - scrollTop,
- };
-
- let { htmlRegion } = placementInfo;
- const VISIBLE = 'visible' as const;
- const VISIBLE_FIRST = 'visibleFirst' as const;
- if (htmlRegion !== 'scroll' && htmlRegion !== VISIBLE_FIRST) {
- htmlRegion = VISIBLE;
- }
- const isVisibleFirst = htmlRegion === VISIBLE_FIRST;
-
- const scrollRegionArea = getVisibleArea(scrollRegion, scrollerList);
- const visibleRegionArea = getVisibleArea(visibleRegion, scrollerList);
-
- const visibleArea =
- htmlRegion === VISIBLE ? visibleRegionArea : scrollRegionArea;
-
- // When set to `visibleFirst`,
- // the check `adjust` logic will use `visibleRegion` for check first.
- const adjustCheckVisibleArea = isVisibleFirst
- ? visibleRegionArea
- : visibleArea;
-
- // Record right & bottom align data
- popupElement.style.left = 'auto';
- popupElement.style.top = 'auto';
- popupElement.style.right = '0';
- popupElement.style.bottom = '0';
-
- const popupMirrorRect = popupElement.getBoundingClientRect();
-
- // Reset back
- popupElement.style.left = originLeft;
- popupElement.style.top = originTop;
- popupElement.style.right = originRight;
- popupElement.style.bottom = originBottom;
- popupElement.style.overflow = originOverflow;
-
- popupElement.parentElement?.removeChild(placeholderElement);
-
- // Calculate scale
- const scaleX = toNum(
- Math.round((popupWidth / parseFloat(width)) * 1000) / 1000,
- );
- const scaleY = toNum(
- Math.round((popupHeight / parseFloat(height)) * 1000) / 1000,
- );
-
- // No need to align since it's not visible in view
- if (
- scaleX === 0 ||
- scaleY === 0 ||
- (isDOM(target) && !isVisible(target))
- ) {
- return;
- }
-
- // Offset
- const { offset, targetOffset } = placementInfo;
- let [popupOffsetX, popupOffsetY] = getNumberOffset(popupRect, offset);
- const [targetOffsetX, targetOffsetY] = getNumberOffset(
- targetRect,
- targetOffset,
- );
-
- targetRect.x -= targetOffsetX;
- targetRect.y -= targetOffsetY;
-
- // Points
- const [popupPoint, targetPoint] = placementInfo.points || [];
- const targetPoints = splitPoints(targetPoint);
- const popupPoints = splitPoints(popupPoint);
-
- const targetAlignPoint = getAlignPoint(targetRect, targetPoints);
- const popupAlignPoint = getAlignPoint(popupRect, popupPoints);
-
- // Real align info may not same as origin one
- const nextAlignInfo = {
- ...placementInfo,
- };
-
- // Next Offset
- let nextOffsetX = targetAlignPoint.x - popupAlignPoint.x + popupOffsetX;
- let nextOffsetY = targetAlignPoint.y - popupAlignPoint.y + popupOffsetY;
-
- // ============== Intersection ===============
- // Get area by position. Used for check if flip area is better
- function getIntersectionVisibleArea(
- offsetX: number,
- offsetY: number,
- area = visibleArea,
- ) {
- const l = popupRect.x + offsetX;
- const t = popupRect.y + offsetY;
-
- const r = l + popupWidth;
- const b = t + popupHeight;
-
- const visibleL = Math.max(l, area.left);
- const visibleT = Math.max(t, area.top);
- const visibleR = Math.min(r, area.right);
- const visibleB = Math.min(b, area.bottom);
-
- return Math.max(0, (visibleR - visibleL) * (visibleB - visibleT));
- }
-
- const originIntersectionVisibleArea = getIntersectionVisibleArea(
- nextOffsetX,
- nextOffsetY,
- );
-
- // As `visibleFirst`, we prepare this for check
- const originIntersectionRecommendArea = getIntersectionVisibleArea(
- nextOffsetX,
- nextOffsetY,
- visibleRegionArea,
- );
-
- // ========================== Overflow ===========================
- const targetAlignPointTL = getAlignPoint(targetRect, ['t', 'l']);
- const popupAlignPointTL = getAlignPoint(popupRect, ['t', 'l']);
- const targetAlignPointBR = getAlignPoint(targetRect, ['b', 'r']);
- const popupAlignPointBR = getAlignPoint(popupRect, ['b', 'r']);
-
- const overflow = placementInfo.overflow || {};
- const { adjustX, adjustY, shiftX, shiftY } = overflow;
-
- const supportAdjust = (val: boolean | number) => {
- if (typeof val === 'boolean') {
- return val;
- }
- return val >= 0;
- };
-
- // Prepare position
- let nextPopupY: number;
- let nextPopupBottom: number;
- let nextPopupX: number;
- let nextPopupRight: number;
-
- function syncNextPopupPosition() {
- nextPopupY = popupRect.y + nextOffsetY;
- nextPopupBottom = nextPopupY + popupHeight;
- nextPopupX = popupRect.x + nextOffsetX;
- nextPopupRight = nextPopupX + popupWidth;
- }
- syncNextPopupPosition();
-
- // >>>>>>>>>> Top & Bottom
- const needAdjustY = supportAdjust(adjustY);
-
- const sameTB = popupPoints[0] === targetPoints[0];
-
- // Bottom to Top
- if (
- needAdjustY &&
- popupPoints[0] === 't' &&
- (nextPopupBottom > adjustCheckVisibleArea.bottom ||
- prevFlipRef.current.bt)
- ) {
- let tmpNextOffsetY: number = nextOffsetY;
-
- if (sameTB) {
- tmpNextOffsetY -= popupHeight - targetHeight;
- } else {
- tmpNextOffsetY =
- targetAlignPointTL.y - popupAlignPointBR.y - popupOffsetY;
- }
-
- const newVisibleArea = getIntersectionVisibleArea(
- nextOffsetX,
- tmpNextOffsetY,
- );
- const newVisibleRecommendArea = getIntersectionVisibleArea(
- nextOffsetX,
- tmpNextOffsetY,
- visibleRegionArea,
- );
-
- if (
- // Of course use larger one
- newVisibleArea > originIntersectionVisibleArea ||
- (newVisibleArea === originIntersectionVisibleArea &&
- (!isVisibleFirst ||
- // Choose recommend one
- newVisibleRecommendArea >= originIntersectionRecommendArea))
- ) {
- prevFlipRef.current.bt = true;
- nextOffsetY = tmpNextOffsetY;
- popupOffsetY = -popupOffsetY;
-
- nextAlignInfo.points = [
- reversePoints(popupPoints, 0),
- reversePoints(targetPoints, 0),
- ];
- } else {
- prevFlipRef.current.bt = false;
- }
- }
-
- // Top to Bottom
- if (
- needAdjustY &&
- popupPoints[0] === 'b' &&
- (nextPopupY < adjustCheckVisibleArea.top || prevFlipRef.current.tb)
- ) {
- let tmpNextOffsetY: number = nextOffsetY;
-
- if (sameTB) {
- tmpNextOffsetY += popupHeight - targetHeight;
- } else {
- tmpNextOffsetY =
- targetAlignPointBR.y - popupAlignPointTL.y - popupOffsetY;
- }
-
- const newVisibleArea = getIntersectionVisibleArea(
- nextOffsetX,
- tmpNextOffsetY,
- );
- const newVisibleRecommendArea = getIntersectionVisibleArea(
- nextOffsetX,
- tmpNextOffsetY,
- visibleRegionArea,
- );
-
- if (
- // Of course use larger one
- newVisibleArea > originIntersectionVisibleArea ||
- (newVisibleArea === originIntersectionVisibleArea &&
- (!isVisibleFirst ||
- // Choose recommend one
- newVisibleRecommendArea >= originIntersectionRecommendArea))
- ) {
- prevFlipRef.current.tb = true;
- nextOffsetY = tmpNextOffsetY;
- popupOffsetY = -popupOffsetY;
-
- nextAlignInfo.points = [
- reversePoints(popupPoints, 0),
- reversePoints(targetPoints, 0),
- ];
- } else {
- prevFlipRef.current.tb = false;
- }
- }
-
- // >>>>>>>>>> Left & Right
- const needAdjustX = supportAdjust(adjustX);
-
- // >>>>> Flip
- const sameLR = popupPoints[1] === targetPoints[1];
-
- // Right to Left
- if (
- needAdjustX &&
- popupPoints[1] === 'l' &&
- (nextPopupRight > adjustCheckVisibleArea.right ||
- prevFlipRef.current.rl)
- ) {
- let tmpNextOffsetX: number = nextOffsetX;
-
- if (sameLR) {
- tmpNextOffsetX -= popupWidth - targetWidth;
- } else {
- tmpNextOffsetX =
- targetAlignPointTL.x - popupAlignPointBR.x - popupOffsetX;
- }
-
- const newVisibleArea = getIntersectionVisibleArea(
- tmpNextOffsetX,
- nextOffsetY,
- );
- const newVisibleRecommendArea = getIntersectionVisibleArea(
- tmpNextOffsetX,
- nextOffsetY,
- visibleRegionArea,
- );
-
- if (
- // Of course use larger one
- newVisibleArea > originIntersectionVisibleArea ||
- (newVisibleArea === originIntersectionVisibleArea &&
- (!isVisibleFirst ||
- // Choose recommend one
- newVisibleRecommendArea >= originIntersectionRecommendArea))
- ) {
- prevFlipRef.current.rl = true;
- nextOffsetX = tmpNextOffsetX;
- popupOffsetX = -popupOffsetX;
-
- nextAlignInfo.points = [
- reversePoints(popupPoints, 1),
- reversePoints(targetPoints, 1),
- ];
- } else {
- prevFlipRef.current.rl = false;
- }
- }
-
- // Left to Right
- if (
- needAdjustX &&
- popupPoints[1] === 'r' &&
- (nextPopupX < adjustCheckVisibleArea.left || prevFlipRef.current.lr)
- ) {
- let tmpNextOffsetX: number = nextOffsetX;
-
- if (sameLR) {
- tmpNextOffsetX += popupWidth - targetWidth;
- } else {
- tmpNextOffsetX =
- targetAlignPointBR.x - popupAlignPointTL.x - popupOffsetX;
- }
-
- const newVisibleArea = getIntersectionVisibleArea(
- tmpNextOffsetX,
- nextOffsetY,
- );
- const newVisibleRecommendArea = getIntersectionVisibleArea(
- tmpNextOffsetX,
- nextOffsetY,
- visibleRegionArea,
- );
-
- if (
- // Of course use larger one
- newVisibleArea > originIntersectionVisibleArea ||
- (newVisibleArea === originIntersectionVisibleArea &&
- (!isVisibleFirst ||
- // Choose recommend one
- newVisibleRecommendArea >= originIntersectionRecommendArea))
- ) {
- prevFlipRef.current.lr = true;
- nextOffsetX = tmpNextOffsetX;
- popupOffsetX = -popupOffsetX;
-
- nextAlignInfo.points = [
- reversePoints(popupPoints, 1),
- reversePoints(targetPoints, 1),
- ];
- } else {
- prevFlipRef.current.lr = false;
- }
- }
-
- // ============================ Shift ============================
- syncNextPopupPosition();
-
- const numShiftX = shiftX === true ? 0 : shiftX;
- if (typeof numShiftX === 'number') {
- // Left
- if (nextPopupX < visibleRegionArea.left) {
- nextOffsetX -= nextPopupX - visibleRegionArea.left - popupOffsetX;
-
- if (targetRect.x + targetWidth < visibleRegionArea.left + numShiftX) {
- nextOffsetX +=
- targetRect.x - visibleRegionArea.left + targetWidth - numShiftX;
- }
- }
-
- // Right
- if (nextPopupRight > visibleRegionArea.right) {
- nextOffsetX -=
- nextPopupRight - visibleRegionArea.right - popupOffsetX;
-
- if (targetRect.x > visibleRegionArea.right - numShiftX) {
- nextOffsetX += targetRect.x - visibleRegionArea.right + numShiftX;
- }
- }
- }
-
- const numShiftY = shiftY === true ? 0 : shiftY;
- if (typeof numShiftY === 'number') {
- // Top
- if (nextPopupY < visibleRegionArea.top) {
- nextOffsetY -= nextPopupY - visibleRegionArea.top - popupOffsetY;
-
- // When target if far away from visible area
- // Stop shift
- if (targetRect.y + targetHeight < visibleRegionArea.top + numShiftY) {
- nextOffsetY +=
- targetRect.y - visibleRegionArea.top + targetHeight - numShiftY;
- }
- }
-
- // Bottom
- if (nextPopupBottom > visibleRegionArea.bottom) {
- nextOffsetY -=
- nextPopupBottom - visibleRegionArea.bottom - popupOffsetY;
-
- if (targetRect.y > visibleRegionArea.bottom - numShiftY) {
- nextOffsetY += targetRect.y - visibleRegionArea.bottom + numShiftY;
- }
- }
- }
-
- // ============================ Arrow ============================
- // Arrow center align
- const popupLeft = popupRect.x + nextOffsetX;
- const popupRight = popupLeft + popupWidth;
- const popupTop = popupRect.y + nextOffsetY;
- const popupBottom = popupTop + popupHeight;
-
- const targetLeft = targetRect.x;
- const targetRight = targetLeft + targetWidth;
- const targetTop = targetRect.y;
- const targetBottom = targetTop + targetHeight;
-
- const maxLeft = Math.max(popupLeft, targetLeft);
- const minRight = Math.min(popupRight, targetRight);
-
- const xCenter = (maxLeft + minRight) / 2;
- const nextArrowX = xCenter - popupLeft;
-
- const maxTop = Math.max(popupTop, targetTop);
- const minBottom = Math.min(popupBottom, targetBottom);
-
- const yCenter = (maxTop + minBottom) / 2;
- const nextArrowY = yCenter - popupTop;
-
- onPopupAlign?.(popupEle, nextAlignInfo);
-
- // Additional calculate right & bottom position
- let offsetX4Right =
- popupMirrorRect.right - popupRect.x - (nextOffsetX + popupRect.width);
- let offsetY4Bottom =
- popupMirrorRect.bottom - popupRect.y - (nextOffsetY + popupRect.height);
-
- if (scaleX === 1) {
- nextOffsetX = Math.round(nextOffsetX);
- offsetX4Right = Math.round(offsetX4Right);
- }
-
- if (scaleY === 1) {
- nextOffsetY = Math.round(nextOffsetY);
- offsetY4Bottom = Math.round(offsetY4Bottom);
- }
-
- const nextOffsetInfo = {
- ready: true,
- offsetX: nextOffsetX / scaleX,
- offsetY: nextOffsetY / scaleY,
- offsetR: offsetX4Right / scaleX,
- offsetB: offsetY4Bottom / scaleY,
- arrowX: nextArrowX / scaleX,
- arrowY: nextArrowY / scaleY,
- scaleX,
- scaleY,
- align: nextAlignInfo,
- };
-
- setOffsetInfo(nextOffsetInfo);
- }
- });
-
- const triggerAlign = () => {
- alignCountRef.current += 1;
- const id = alignCountRef.current;
-
- // Merge all align requirement into one frame
- Promise.resolve().then(() => {
- if (alignCountRef.current === id) {
- onAlign();
- }
- });
- };
-
- // Reset ready status when placement & open changed
- const resetReady = () => {
- setOffsetInfo((ori) => ({
- ...ori,
- ready: false,
- }));
- };
-
- useLayoutEffect(resetReady, [placement]);
-
- useLayoutEffect(() => {
- if (!open) {
- resetReady();
- }
- }, [open]);
-
- return [
- offsetInfo.ready,
- offsetInfo.offsetX,
- offsetInfo.offsetY,
- offsetInfo.offsetR,
- offsetInfo.offsetB,
- offsetInfo.arrowX,
- offsetInfo.arrowY,
- offsetInfo.scaleX,
- offsetInfo.scaleY,
- offsetInfo.align,
- triggerAlign,
- ];
-}
diff --git a/src/hooks/useQRCode.tsx b/src/hooks/useQRCode.tsx
new file mode 100644
index 0000000..e60ac8f
--- /dev/null
+++ b/src/hooks/useQRCode.tsx
@@ -0,0 +1,53 @@
+import { QrCode, QrSegment } from '../libs/qrcodegen';
+import type { ErrorCorrectionLevel, ImageSettings } from '../interface';
+import { ERROR_LEVEL_MAP, getImageSettings, getMarginSize } from '../utils';
+import { useMemo } from 'react';
+
+export function useQRCode({
+ value,
+ level,
+ minVersion,
+ includeMargin,
+ marginSize,
+ imageSettings,
+ size,
+}: {
+ value: string;
+ level: ErrorCorrectionLevel;
+ minVersion: number;
+ includeMargin: boolean;
+ marginSize?: number;
+ imageSettings?: ImageSettings;
+ size: number;
+}) {
+ const qrcode = useMemo(() => {
+ const segments = QrSegment.makeSegments(value);
+ return QrCode.encodeSegments(
+ segments,
+ ERROR_LEVEL_MAP[level],
+ minVersion,
+ );
+ }, [value, level, minVersion]);
+
+ const { cells, margin, numCells, calculatedImageSettings } = useMemo(() => {
+ const cs = qrcode.getModules();
+
+ const mg = getMarginSize(includeMargin, marginSize);
+ const ncs = cs.length + mg * 2;
+ const cis = getImageSettings(cs, size, mg, imageSettings);
+ return {
+ cells: cs,
+ margin: mg,
+ numCells: ncs,
+ calculatedImageSettings: cis,
+ };
+ }, [qrcode, size, imageSettings, includeMargin, marginSize]);
+
+ return {
+ qrcode,
+ margin,
+ cells,
+ numCells,
+ calculatedImageSettings,
+ };
+}
diff --git a/src/hooks/useWatch.ts b/src/hooks/useWatch.ts
deleted file mode 100644
index eba0fd8..0000000
--- a/src/hooks/useWatch.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
-import { collectScroller, getWin } from '../util';
-
-export default function useWatch(
- open: boolean,
- target: HTMLElement,
- popup: HTMLElement,
- onAlign: VoidFunction,
- onScroll: VoidFunction,
-) {
- useLayoutEffect(() => {
- if (open && target && popup) {
- const targetElement = target;
- const popupElement = popup;
- const targetScrollList = collectScroller(targetElement);
- const popupScrollList = collectScroller(popupElement);
-
- const win = getWin(popupElement);
-
- const mergedList = new Set([
- win,
- ...targetScrollList,
- ...popupScrollList,
- ]);
-
- function notifyScroll() {
- onAlign();
- onScroll();
- }
-
- mergedList.forEach((scroller) => {
- scroller.addEventListener('scroll', notifyScroll, { passive: true });
- });
-
- win.addEventListener('resize', notifyScroll, { passive: true });
-
- // First time always do align
- onAlign();
-
- return () => {
- mergedList.forEach((scroller) => {
- scroller.removeEventListener('scroll', notifyScroll);
- win.removeEventListener('resize', notifyScroll);
- });
- };
- }
- }, [open, target, popup]);
-}
diff --git a/src/hooks/useWinClick.ts b/src/hooks/useWinClick.ts
deleted file mode 100644
index f33f05b..0000000
--- a/src/hooks/useWinClick.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { getShadowRoot } from 'rc-util/lib/Dom/shadow';
-import { warning } from 'rc-util/lib/warning';
-import * as React from 'react';
-import { getWin } from '../util';
-
-export default function useWinClick(
- open: boolean,
- clickToHide: boolean,
- targetEle: HTMLElement,
- popupEle: HTMLElement,
- mask: boolean,
- maskClosable: boolean,
- inPopupOrChild: (target: EventTarget) => boolean,
- triggerOpen: (open: boolean) => void,
-) {
- const openRef = React.useRef(open);
- openRef.current = open;
-
- // Click to hide is special action since click popup element should not hide
- React.useEffect(() => {
- if (clickToHide && popupEle && (!mask || maskClosable)) {
- const onTriggerClose = ({ target }: MouseEvent) => {
- if (openRef.current && !inPopupOrChild(target)) {
- triggerOpen(false);
- }
- };
-
- const win = getWin(popupEle);
-
- win.addEventListener('mousedown', onTriggerClose, true);
- win.addEventListener('contextmenu', onTriggerClose, true);
-
- // shadow root
- const targetShadowRoot = getShadowRoot(targetEle);
- if (targetShadowRoot) {
- targetShadowRoot.addEventListener('mousedown', onTriggerClose, true);
- targetShadowRoot.addEventListener('contextmenu', onTriggerClose, true);
- }
-
- // Warning if target and popup not in same root
- if (process.env.NODE_ENV !== 'production') {
- const targetRoot = targetEle?.getRootNode?.();
- const popupRoot = popupEle.getRootNode?.();
-
- warning(
- targetRoot === popupRoot,
- `trigger element and popup element should in same shadow root.`,
- );
- }
-
- return () => {
- win.removeEventListener('mousedown', onTriggerClose, true);
- win.removeEventListener('contextmenu', onTriggerClose, true);
-
- if (targetShadowRoot) {
- targetShadowRoot.removeEventListener(
- 'mousedown',
- onTriggerClose,
- true,
- );
- targetShadowRoot.removeEventListener(
- 'contextmenu',
- onTriggerClose,
- true,
- );
- }
- };
- }
- }, [clickToHide, targetEle, popupEle, mask, maskClosable]);
-}
diff --git a/src/index.tsx b/src/index.tsx
index 4a7b990..5367d99 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,763 +1,4 @@
-import Portal from '@rc-component/portal';
-import classNames from 'classnames';
-import type { CSSMotionProps } from 'rc-motion';
-import ResizeObserver from 'rc-resize-observer';
-import { isDOM } from 'rc-util/lib/Dom/findDOMNode';
-import { getShadowRoot } from 'rc-util/lib/Dom/shadow';
-import useEvent from 'rc-util/lib/hooks/useEvent';
-import useId from 'rc-util/lib/hooks/useId';
-import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
-import isMobile from 'rc-util/lib/isMobile';
-import * as React from 'react';
-import Popup from './Popup';
-import TriggerWrapper from './TriggerWrapper';
-import type { TriggerContextProps } from './context';
-import TriggerContext from './context';
-import useAction from './hooks/useAction';
-import useAlign from './hooks/useAlign';
-import useWatch from './hooks/useWatch';
-import useWinClick from './hooks/useWinClick';
-import type {
- ActionType,
- AlignType,
- AnimationType,
- ArrowPos,
- ArrowTypeOuter,
- BuildInPlacements,
- TransitionNameType,
-} from './interface';
-import { getAlignPopupClassName, getMotion } from './util';
-
-export type {
- ActionType,
- AlignType,
- ArrowTypeOuter as ArrowType,
- BuildInPlacements,
-};
-
-export interface TriggerRef {
- nativeElement: HTMLElement;
- popupElement: HTMLDivElement;
- forceAlign: VoidFunction;
-}
-
-// Removed Props List
-// Seems this can be auto
-// getDocument?: (element?: HTMLElement) => Document;
-
-// New version will not wrap popup with `rc-trigger-popup-content` when multiple children
-
-export interface TriggerProps {
- children: React.ReactElement;
- action?: ActionType | ActionType[];
- showAction?: ActionType[];
- hideAction?: ActionType[];
-
- prefixCls?: string;
-
- zIndex?: number;
-
- onPopupAlign?: (element: HTMLElement, align: AlignType) => void;
-
- stretch?: string;
-
- // ==================== Open =====================
- popupVisible?: boolean;
- defaultPopupVisible?: boolean;
- onPopupVisibleChange?: (visible: boolean) => void;
- afterPopupVisibleChange?: (visible: boolean) => void;
-
- // =================== Portal ====================
- getPopupContainer?: (node: HTMLElement) => HTMLElement;
- forceRender?: boolean;
- autoDestroy?: boolean;
-
- /** @deprecated Please use `autoDestroy` instead */
- destroyPopupOnHide?: boolean;
-
- // ==================== Mask =====================
- mask?: boolean;
- maskClosable?: boolean;
-
- // =================== Motion ====================
- /** Set popup motion. You can ref `rc-motion` for more info. */
- popupMotion?: CSSMotionProps;
- /** Set mask motion. You can ref `rc-motion` for more info. */
- maskMotion?: CSSMotionProps;
-
- /** @deprecated Please us `popupMotion` instead. */
- popupTransitionName?: TransitionNameType;
- /** @deprecated Please us `popupMotion` instead. */
- popupAnimation?: AnimationType;
- /** @deprecated Please us `maskMotion` instead. */
- maskTransitionName?: TransitionNameType;
- /** @deprecated Please us `maskMotion` instead. */
- maskAnimation?: AnimationType;
-
- // ==================== Delay ====================
- mouseEnterDelay?: number;
- mouseLeaveDelay?: number;
-
- focusDelay?: number;
- blurDelay?: number;
-
- // ==================== Popup ====================
- popup: React.ReactNode | (() => React.ReactNode);
- popupPlacement?: string;
- builtinPlacements?: BuildInPlacements;
- popupAlign?: AlignType;
- popupClassName?: string;
- popupStyle?: React.CSSProperties;
- getPopupClassNameFromAlign?: (align: AlignType) => string;
- onPopupClick?: React.MouseEventHandler;
-
- alignPoint?: boolean; // Maybe we can support user pass position in the future
-
- /**
- * Trigger will memo content when close.
- * This may affect the case if want to keep content update.
- * Set `fresh` to `false` will always keep update.
- */
- fresh?: boolean;
-
- // ==================== Arrow ====================
- arrow?: boolean | ArrowTypeOuter;
-
- // ================= Deprecated ==================
- /** @deprecated Add `className` on `children`. Please add `className` directly instead. */
- className?: string;
-
- // =================== Private ===================
- /**
- * @private Get trigger DOM node.
- * Used for some component is function component which can not access by `findDOMNode`
- */
- getTriggerDOMNode?: (node: React.ReactInstance) => HTMLElement;
-
- // // ========================== Mobile ==========================
- // /** @private Bump fixed position at bottom in mobile.
- // * This is internal usage currently, do not use in your prod */
- // mobile?: MobileConfig;
-}
-
-export function generateTrigger(
- PortalComponent: React.ComponentType = Portal,
-) {
- const Trigger = React.forwardRef((props, ref) => {
- const {
- prefixCls = 'rc-trigger-popup',
- children,
-
- // Action
- action = 'hover',
- showAction,
- hideAction,
-
- // Open
- popupVisible,
- defaultPopupVisible,
- onPopupVisibleChange,
- afterPopupVisibleChange,
-
- // Delay
- mouseEnterDelay,
- mouseLeaveDelay = 0.1,
-
- focusDelay,
- blurDelay,
-
- // Mask
- mask,
- maskClosable = true,
-
- // Portal
- getPopupContainer,
- forceRender,
- autoDestroy,
- destroyPopupOnHide,
-
- // Popup
- popup,
- popupClassName,
- popupStyle,
-
- popupPlacement,
- builtinPlacements = {},
- popupAlign,
- zIndex,
- stretch,
- getPopupClassNameFromAlign,
- fresh,
-
- alignPoint,
-
- onPopupClick,
- onPopupAlign,
-
- // Arrow
- arrow,
-
- // Motion
- popupMotion,
- maskMotion,
- popupTransitionName,
- popupAnimation,
- maskTransitionName,
- maskAnimation,
-
- // Deprecated
- className,
-
- // Private
- getTriggerDOMNode,
-
- ...restProps
- } = props;
-
- const mergedAutoDestroy = autoDestroy || destroyPopupOnHide || false;
-
- // =========================== Mobile ===========================
- const [mobile, setMobile] = React.useState(false);
- useLayoutEffect(() => {
- setMobile(isMobile());
- }, []);
-
- // ========================== Context ===========================
- const subPopupElements = React.useRef>({});
-
- const parentContext = React.useContext(TriggerContext);
- const context = React.useMemo(() => {
- return {
- registerSubPopup: (id, subPopupEle) => {
- subPopupElements.current[id] = subPopupEle;
-
- parentContext?.registerSubPopup(id, subPopupEle);
- },
- };
- }, [parentContext]);
-
- // =========================== Popup ============================
- const id = useId();
- const [popupEle, setPopupEle] = React.useState(null);
-
- // Used for forwardRef popup. Not use internal
- const externalPopupRef = React.useRef(null);
-
- const setPopupRef = useEvent((node: HTMLDivElement) => {
- externalPopupRef.current = node;
-
- if (isDOM(node) && popupEle !== node) {
- setPopupEle(node);
- }
-
- parentContext?.registerSubPopup(id, node);
- });
-
- // =========================== Target ===========================
- // Use state to control here since `useRef` update not trigger render
- const [targetEle, setTargetEle] = React.useState(null);
-
- // Used for forwardRef target. Not use internal
- const externalForwardRef = React.useRef(null);
-
- const setTargetRef = useEvent((node: HTMLElement) => {
- if (isDOM(node) && targetEle !== node) {
- setTargetEle(node);
- externalForwardRef.current = node;
- }
- });
-
- // ========================== Children ==========================
- const child = React.Children.only(children) as React.ReactElement;
- const originChildProps = child?.props || {};
- const cloneProps: typeof originChildProps = {};
-
- const inPopupOrChild = useEvent((ele: any) => {
- const childDOM = targetEle;
-
- return (
- childDOM?.contains(ele) ||
- getShadowRoot(childDOM)?.host === ele ||
- ele === childDOM ||
- popupEle?.contains(ele) ||
- getShadowRoot(popupEle)?.host === ele ||
- ele === popupEle ||
- Object.values(subPopupElements.current).some(
- (subPopupEle) => subPopupEle?.contains(ele) || ele === subPopupEle,
- )
- );
- });
-
- // =========================== Motion ===========================
- const mergePopupMotion = getMotion(
- prefixCls,
- popupMotion,
- popupAnimation,
- popupTransitionName,
- );
-
- const mergeMaskMotion = getMotion(
- prefixCls,
- maskMotion,
- maskAnimation,
- maskTransitionName,
- );
-
- // ============================ Open ============================
- const [internalOpen, setInternalOpen] = React.useState(
- defaultPopupVisible || false,
- );
-
- // Render still use props as first priority
- const mergedOpen = popupVisible ?? internalOpen;
-
- // We use effect sync here in case `popupVisible` back to `undefined`
- const setMergedOpen = useEvent((nextOpen: boolean) => {
- if (popupVisible === undefined) {
- setInternalOpen(nextOpen);
- }
- });
-
- useLayoutEffect(() => {
- setInternalOpen(popupVisible || false);
- }, [popupVisible]);
-
- const openRef = React.useRef(mergedOpen);
- openRef.current = mergedOpen;
-
- const lastTriggerRef = React.useRef([]);
- lastTriggerRef.current = [];
-
- const internalTriggerOpen = useEvent((nextOpen: boolean) => {
- setMergedOpen(nextOpen);
-
- // Enter or Pointer will both trigger open state change
- // We only need take one to avoid duplicated change event trigger
- // Use `lastTriggerRef` to record last open type
- if (
- (lastTriggerRef.current[lastTriggerRef.current.length - 1] ??
- mergedOpen) !== nextOpen
- ) {
- lastTriggerRef.current.push(nextOpen);
- onPopupVisibleChange?.(nextOpen);
- }
- });
-
- // Trigger for delay
- const delayRef = React.useRef();
-
- const clearDelay = () => {
- clearTimeout(delayRef.current);
- };
-
- const triggerOpen = (nextOpen: boolean, delay = 0) => {
- clearDelay();
-
- if (delay === 0) {
- internalTriggerOpen(nextOpen);
- } else {
- delayRef.current = setTimeout(() => {
- internalTriggerOpen(nextOpen);
- }, delay * 1000);
- }
- };
-
- React.useEffect(() => clearDelay, []);
-
- // ========================== Motion ============================
- const [inMotion, setInMotion] = React.useState(false);
-
- useLayoutEffect(
- (firstMount) => {
- if (!firstMount || mergedOpen) {
- setInMotion(true);
- }
- },
- [mergedOpen],
- );
-
- const [motionPrepareResolve, setMotionPrepareResolve] =
- React.useState(null);
-
- // =========================== Align ============================
- const [mousePos, setMousePos] = React.useState<[x: number, y: number]>([
- 0, 0,
- ]);
-
- const setMousePosByEvent = (
- event: Pick,
- ) => {
- setMousePos([event.clientX, event.clientY]);
- };
-
- const [
- ready,
- offsetX,
- offsetY,
- offsetR,
- offsetB,
- arrowX,
- arrowY,
- scaleX,
- scaleY,
- alignInfo,
- onAlign,
- ] = useAlign(
- mergedOpen,
- popupEle,
- alignPoint ? mousePos : targetEle,
- popupPlacement,
- builtinPlacements,
- popupAlign,
- onPopupAlign,
- );
-
- const [showActions, hideActions] = useAction(
- mobile,
- action,
- showAction,
- hideAction,
- );
-
- const clickToShow = showActions.has('click');
- const clickToHide =
- hideActions.has('click') || hideActions.has('contextMenu');
-
- const triggerAlign = useEvent(() => {
- if (!inMotion) {
- onAlign();
- }
- });
-
- const onScroll = () => {
- if (openRef.current && alignPoint && clickToHide) {
- triggerOpen(false);
- }
- };
-
- useWatch(mergedOpen, targetEle, popupEle, triggerAlign, onScroll);
-
- useLayoutEffect(() => {
- triggerAlign();
- }, [mousePos, popupPlacement]);
-
- // When no builtinPlacements and popupAlign changed
- useLayoutEffect(() => {
- if (mergedOpen && !builtinPlacements?.[popupPlacement]) {
- triggerAlign();
- }
- }, [JSON.stringify(popupAlign)]);
-
- const alignedClassName = React.useMemo(() => {
- const baseClassName = getAlignPopupClassName(
- builtinPlacements,
- prefixCls,
- alignInfo,
- alignPoint,
- );
-
- return classNames(baseClassName, getPopupClassNameFromAlign?.(alignInfo));
- }, [
- alignInfo,
- getPopupClassNameFromAlign,
- builtinPlacements,
- prefixCls,
- alignPoint,
- ]);
-
- // ============================ Refs ============================
- React.useImperativeHandle(ref, () => ({
- nativeElement: externalForwardRef.current,
- popupElement: externalPopupRef.current,
- forceAlign: triggerAlign,
- }));
-
- // ========================== Stretch ===========================
- const [targetWidth, setTargetWidth] = React.useState(0);
- const [targetHeight, setTargetHeight] = React.useState(0);
-
- const syncTargetSize = () => {
- if (stretch && targetEle) {
- const rect = targetEle.getBoundingClientRect();
- setTargetWidth(rect.width);
- setTargetHeight(rect.height);
- }
- };
-
- const onTargetResize = () => {
- syncTargetSize();
- triggerAlign();
- };
-
- // ========================== Motion ============================
- const onVisibleChanged = (visible: boolean) => {
- setInMotion(false);
- onAlign();
- afterPopupVisibleChange?.(visible);
- };
-
- // We will trigger align when motion is in prepare
- const onPrepare = () =>
- new Promise((resolve) => {
- syncTargetSize();
- setMotionPrepareResolve(() => resolve);
- });
-
- useLayoutEffect(() => {
- if (motionPrepareResolve) {
- onAlign();
- motionPrepareResolve();
- setMotionPrepareResolve(null);
- }
- }, [motionPrepareResolve]);
-
- // =========================== Action ===========================
- /**
- * Util wrapper for trigger action
- */
- function wrapperAction(
- eventName: string,
- nextOpen: boolean,
- delay?: number,
- preEvent?: (event: Event) => void,
- ) {
- cloneProps[eventName] = (event: any, ...args: any[]) => {
- preEvent?.(event);
- triggerOpen(nextOpen, delay);
-
- // Pass to origin
- originChildProps[eventName]?.(event, ...args);
- };
- }
-
- // ======================= Action: Click ========================
- if (clickToShow || clickToHide) {
- cloneProps.onClick = (
- event: React.MouseEvent,
- ...args: any[]
- ) => {
- if (openRef.current && clickToHide) {
- triggerOpen(false);
- } else if (!openRef.current && clickToShow) {
- setMousePosByEvent(event);
- triggerOpen(true);
- }
-
- // Pass to origin
- originChildProps.onClick?.(event, ...args);
- };
- }
-
- // Click to hide is special action since click popup element should not hide
- useWinClick(
- mergedOpen,
- clickToHide,
- targetEle,
- popupEle,
- mask,
- maskClosable,
- inPopupOrChild,
- triggerOpen,
- );
-
- // ======================= Action: Hover ========================
- const hoverToShow = showActions.has('hover');
- const hoverToHide = hideActions.has('hover');
-
- let onPopupMouseEnter: React.MouseEventHandler;
- let onPopupMouseLeave: VoidFunction;
-
- if (hoverToShow) {
- // Compatible with old browser which not support pointer event
- wrapperAction(
- 'onMouseEnter',
- true,
- mouseEnterDelay,
- (event) => {
- setMousePosByEvent(event);
- },
- );
- wrapperAction(
- 'onPointerEnter',
- true,
- mouseEnterDelay,
- (event) => {
- setMousePosByEvent(event);
- },
- );
- onPopupMouseEnter = (event) => {
- // Only trigger re-open when popup is visible
- if (
- (mergedOpen || inMotion) &&
- popupEle?.contains(event.target as HTMLElement)
- ) {
- triggerOpen(true, mouseEnterDelay);
- }
- };
-
- // Align Point
- if (alignPoint) {
- cloneProps.onMouseMove = (event: React.MouseEvent) => {
- // setMousePosByEvent(event);
- originChildProps.onMouseMove?.(event);
- };
- }
- }
-
- if (hoverToHide) {
- wrapperAction('onMouseLeave', false, mouseLeaveDelay);
- wrapperAction('onPointerLeave', false, mouseLeaveDelay);
- onPopupMouseLeave = () => {
- triggerOpen(false, mouseLeaveDelay);
- };
- }
-
- // ======================= Action: Focus ========================
- if (showActions.has('focus')) {
- wrapperAction('onFocus', true, focusDelay);
- }
-
- if (hideActions.has('focus')) {
- wrapperAction('onBlur', false, blurDelay);
- }
-
- // ==================== Action: ContextMenu =====================
- if (showActions.has('contextMenu')) {
- cloneProps.onContextMenu = (event: React.MouseEvent, ...args: any[]) => {
- if (openRef.current && hideActions.has('contextMenu')) {
- triggerOpen(false);
- } else {
- setMousePosByEvent(event);
- triggerOpen(true);
- }
-
- event.preventDefault();
-
- // Pass to origin
- originChildProps.onContextMenu?.(event, ...args);
- };
- }
-
- // ========================= ClassName ==========================
- if (className) {
- cloneProps.className = classNames(originChildProps.className, className);
- }
-
- // =========================== Render ===========================
- const mergedChildrenProps = {
- ...originChildProps,
- ...cloneProps,
- };
-
- // Pass props into cloneProps for nest usage
- const passedProps: Record = {};
- const passedEventList = [
- 'onContextMenu',
- 'onClick',
- 'onMouseDown',
- 'onTouchStart',
- 'onMouseEnter',
- 'onMouseLeave',
- 'onFocus',
- 'onBlur',
- ];
-
- passedEventList.forEach((eventName) => {
- if (restProps[eventName]) {
- passedProps[eventName] = (...args: any[]) => {
- mergedChildrenProps[eventName]?.(...args);
- restProps[eventName](...args);
- };
- }
- });
-
- // Child Node
- const triggerNode = React.cloneElement(child, {
- ...mergedChildrenProps,
- ...passedProps,
- });
-
- const arrowPos: ArrowPos = {
- x: arrowX,
- y: arrowY,
- };
-
- const innerArrow: ArrowTypeOuter = arrow
- ? {
- // true and Object likely
- ...(arrow !== true ? arrow : {}),
- }
- : null;
-
- // Render
- return (
- <>
-
-
- {triggerNode}
-
-
-
-
-
- >
- );
- });
-
- if (process.env.NODE_ENV !== 'production') {
- Trigger.displayName = 'Trigger';
- }
-
- return Trigger;
-}
-
-export default generateTrigger(Portal);
+export type * from './interface';
+export * from './utils';
+export * from './QRCodeCanvas';
+export * from './QRCodeSVG';
diff --git a/src/interface.ts b/src/interface.ts
index ddb27ee..7892e50 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -1,128 +1,40 @@
-import type { CSSMotionProps } from 'rc-motion';
+import type { CSSProperties } from 'react';
+import type { Ecc, QrCode } from './libs/qrcodegen';
-export type Placement =
- | 'top'
- | 'left'
- | 'right'
- | 'bottom'
- | 'topLeft'
- | 'topRight'
- | 'bottomLeft'
- | 'bottomRight'
- | 'leftTop'
- | 'leftBottom'
- | 'rightTop'
- | 'rightBottom';
+export type Modules = ReturnType;
+export type Excavation = { x: number; y: number; w: number; h: number };
+export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H';
+export type CrossOrigin = 'anonymous' | 'use-credentials' | '' | undefined;
-export type AlignPointTopBottom = 't' | 'b' | 'c';
-export type AlignPointLeftRight = 'l' | 'r' | 'c';
-
-/** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */
-export type AlignPoint = `${AlignPointTopBottom}${AlignPointLeftRight}`;
-
-export type OffsetType = number | `${number}%`;
-
-export interface AlignType {
- /**
- * move point of source node to align with point of target node.
- * Such as ['tr','cc'], align top right point of source node with center point of target node.
- * Point can be 't'(top), 'b'(bottom), 'c'(center), 'l'(left), 'r'(right) */
- points?: (string | AlignPoint)[];
-
- /**
- * @private Do not use in your production code
- */
- _experimental?: Record;
-
- /**
- * offset source node by offset[0] in x and offset[1] in y.
- * If offset contains percentage string value, it is relative to sourceNode region.
- */
- offset?: OffsetType[];
- /**
- * offset target node by offset[0] in x and offset[1] in y.
- * If targetOffset contains percentage string value, it is relative to targetNode region.
- */
- targetOffset?: OffsetType[];
- /**
- * If adjustX field is true, will adjust source node in x direction if source node is invisible.
- * If adjustY field is true, will adjust source node in y direction if source node is invisible.
- */
- overflow?: {
- adjustX?: boolean | number;
- adjustY?: boolean | number;
- shiftX?: boolean | number;
- shiftY?: boolean | number;
- };
- /** Auto adjust arrow position */
- autoArrow?: boolean;
- /**
- * Config visible region check of html node. Default `visible`:
- * - `visible`:
- * The visible region of user browser window.
- * Use `clientHeight` for check.
- * If `visible` region not satisfy, fallback to `scroll`.
- * - `scroll`:
- * The whole region of the html scroll area.
- * Use `scrollHeight` for check.
- * - `visibleFirst`:
- * Similar to `visible`, but if `visible` region not satisfy, fallback to `scroll`.
- */
- htmlRegion?: 'visible' | 'scroll' | 'visibleFirst';
-
- /**
- * Auto chose position with `top` or `bottom` by the align result
- */
- dynamicInset?: boolean;
- /**
- * Whether use css right instead of left to position
- */
- useCssRight?: boolean;
- /**
- * Whether use css bottom instead of top to position
- */
- useCssBottom?: boolean;
- /**
- * Whether use css transform instead of left/top/right/bottom to position if browser supports.
- * Defaults to false.
- */
- useCssTransform?: boolean;
- ignoreShake?: boolean;
-}
-
-export interface ArrowTypeOuter {
- className?: string;
- content?: React.ReactNode;
-}
-
-export type ArrowPos = {
- x?: number;
- y?: number;
+export type ERROR_LEVEL_MAPPED_TYPE = {
+ [index in ErrorCorrectionLevel]: Ecc;
};
-export type BuildInPlacements = Record;
-export type StretchType = string;
-
-export type ActionType = 'hover' | 'focus' | 'click' | 'contextMenu';
-
-export type AnimationType = string;
-
-export type TransitionNameType = string;
-
-export interface Point {
- pageX: number;
- pageY: number;
-}
-
-export interface CommonEventHandler {
- remove: () => void;
-}
-
-export interface MobileConfig {
- /** Set popup motion. You can ref `rc-motion` for more info. */
- popupMotion?: CSSMotionProps;
- popupClassName?: string;
- popupStyle?: React.CSSProperties;
- popupRender?: (originNode: React.ReactNode) => React.ReactNode;
-}
+export type ImageSettings = {
+ src: string;
+ height: number;
+ width: number;
+ excavate: boolean;
+ x?: number;
+ y?: number;
+ opacity?: number;
+ crossOrigin?: CrossOrigin;
+ };
+
+ export type QRProps = {
+ value: string;
+ size?: number;
+ level?: ErrorCorrectionLevel;
+ bgColor?: string;
+ fgColor?: string;
+ style?: CSSProperties;
+ includeMargin?: boolean;
+ marginSize?: number;
+ imageSettings?: ImageSettings;
+ title?: string;
+ minVersion?: number;
+ };
+ export type QRPropsCanvas = QRProps & React.CanvasHTMLAttributes;
+ export type QRPropsSVG = QRProps & React.SVGAttributes;
+
\ No newline at end of file
diff --git a/src/libs/qrcodegen.ts b/src/libs/qrcodegen.ts
new file mode 100644
index 0000000..a18be8f
--- /dev/null
+++ b/src/libs/qrcodegen.ts
@@ -0,0 +1,1025 @@
+/* eslint-disable strict */
+/* eslint-disable no-param-reassign */
+/* eslint-disable @typescript-eslint/no-parameter-properties */
+/* eslint-disable @typescript-eslint/no-namespace */
+/**
+ * @license QR Code generator library (TypeScript)
+ * Copyright (c) Project Nayuki.
+ * SPDX-License-Identifier: MIT
+ */
+
+/*---- Data segment class ----*/
+
+type bit = number;
+type byte = number;
+type int = number;
+
+// Appends the given number of low-order bits of the given value
+// to the given buffer. Requires 0 <= len <= 31 and 0 <= val < 2^len.
+function appendBits(val: int, len: int, bb: bit[]): void {
+ if (len < 0 || len > 31 || val >>> len != 0)
+ throw new RangeError('Value out of range');
+ for (
+ let i = len - 1;
+ i >= 0;
+ i-- // Append bit by bit
+ )
+ bb.push((val >>> i) & 1);
+}
+
+// Returns true iff the i'th bit of x is set to 1.
+function getBit(x: int, i: int): boolean {
+ return ((x >>> i) & 1) != 0;
+}
+
+// Throws an exception if the given condition is false.
+function assert(cond: boolean): void {
+ if (!cond) throw new Error('Assertion error');
+}
+
+/*---- Public helper enumeration ----*/
+/*
+ * Describes how a segment's data bits are interpreted. Immutable.
+ */
+export class Mode {
+ /*-- Constants --*/
+
+ public static readonly NUMERIC = new Mode(0x1, [10, 12, 14]);
+ public static readonly ALPHANUMERIC = new Mode(0x2, [9, 11, 13]);
+ public static readonly BYTE = new Mode(0x4, [8, 16, 16]);
+ public static readonly KANJI = new Mode(0x8, [8, 10, 12]);
+ public static readonly ECI = new Mode(0x7, [0, 0, 0]);
+
+ /*-- Constructor and fields --*/
+
+ // The mode indicator bits, which is a uint4 value (range 0 to 15).
+ public modeBits: int;
+ // Number of character count bits for three different version ranges.
+ private numBitsCharCount: [int, int, int];
+
+ private constructor(modeBits: int, numBitsCharCount: [int, int, int]) {
+ this.modeBits = modeBits;
+ this.numBitsCharCount = numBitsCharCount;
+ }
+
+ /*-- Method --*/
+
+ // (Package-private) Returns the bit width of the character count field for a segment in
+ // this mode in a QR Code at the given version number. The result is in the range [0, 16].
+ public numCharCountBits(ver: int): int {
+ return this.numBitsCharCount[Math.floor((ver + 7) / 17)];
+ }
+}
+
+/*---- Public helper enumeration ----*/
+
+/*
+ * The error correction level in a QR Code symbol. Immutable.
+ */
+export class Ecc {
+ /*-- Constants --*/
+
+ public static readonly LOW = new Ecc(0, 1); // The QR Code can tolerate about 7% erroneous codewords
+ public static readonly MEDIUM = new Ecc(1, 0); // The QR Code can tolerate about 15% erroneous codewords
+ public static readonly QUARTILE = new Ecc(2, 3); // The QR Code can tolerate about 25% erroneous codewords
+ public static readonly HIGH = new Ecc(3, 2); // The QR Code can tolerate about 30% erroneous codewords
+
+ /*-- Constructor and fields --*/
+ // In the range 0 to 3 (unsigned 2-bit integer).
+ public ordinal: int;
+ // (Package-private) In the range 0 to 3 (unsigned 2-bit integer).
+ public formatBits: int;
+
+ private constructor(ordinal: int, formatBits: int) {
+ this.ordinal = ordinal;
+ this.formatBits = formatBits;
+ }
+}
+
+/*
+ * A segment of character/binary/control data in a QR Code symbol.
+ * Instances of this class are immutable.
+ * The mid-level way to create a segment is to take the payload data
+ * and call a static factory function such as QrSegment.makeNumeric().
+ * The low-level way to create a segment is to custom-make the bit buffer
+ * and call the QrSegment() constructor with appropriate values.
+ * This segment class imposes no length restrictions, but QR Codes have restrictions.
+ * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
+ * Any segment longer than this is meaningless for the purpose of generating QR Codes.
+ */
+export class QrSegment {
+ /*-- Static factory functions (mid level) --*/
+
+ // Returns a segment representing the given binary data encoded in
+ // byte mode. All input byte arrays are acceptable. Any text string
+ // can be converted to UTF-8 bytes and encoded as a byte mode segment.
+ public static makeBytes(data: Readonly): QrSegment {
+ const bb: bit[] = [];
+ for (const b of data) appendBits(b, 8, bb);
+ return new QrSegment(Mode.BYTE, data.length, bb);
+ }
+
+ // Returns a segment representing the given string of decimal digits encoded in numeric mode.
+ public static makeNumeric(digits: string): QrSegment {
+ if (!QrSegment.isNumeric(digits))
+ throw new RangeError('String contains non-numeric characters');
+ const bb: bit[] = [];
+ for (let i = 0; i < digits.length; ) {
+ // Consume up to 3 digits per iteration
+ const n: int = Math.min(digits.length - i, 3);
+ appendBits(parseInt(digits.substring(i, i + n), 10), n * 3 + 1, bb);
+ i += n;
+ }
+ return new QrSegment(Mode.NUMERIC, digits.length, bb);
+ }
+
+ // Returns a segment representing the given text string encoded in alphanumeric mode.
+ // The characters allowed are: 0 to 9, A to Z (uppercase only), space,
+ // dollar, percent, asterisk, plus, hyphen, period, slash, colon.
+ public static makeAlphanumeric(text: string): QrSegment {
+ if (!QrSegment.isAlphanumeric(text))
+ throw new RangeError(
+ 'String contains unencodable characters in alphanumeric mode',
+ );
+ const bb: bit[] = [];
+ let i: int;
+ for (i = 0; i + 2 <= text.length; i += 2) {
+ // Process groups of 2
+ let temp: int =
+ QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)) * 45;
+ temp += QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i + 1));
+ appendBits(temp, 11, bb);
+ }
+ if (i < text.length)
+ // 1 character remaining
+ appendBits(QrSegment.ALPHANUMERIC_CHARSET.indexOf(text.charAt(i)), 6, bb);
+ return new QrSegment(Mode.ALPHANUMERIC, text.length, bb);
+ }
+
+ // Returns a new mutable list of zero or more segments to represent the given Unicode text string.
+ // The result may use various segment modes and switch modes to optimize the length of the bit stream.
+ public static makeSegments(text: string): QrSegment[] {
+ // Select the most efficient segment encoding automatically
+ if (text == '') return [];
+ else if (QrSegment.isNumeric(text)) return [QrSegment.makeNumeric(text)];
+ else if (QrSegment.isAlphanumeric(text))
+ return [QrSegment.makeAlphanumeric(text)];
+ else return [QrSegment.makeBytes(QrSegment.toUtf8ByteArray(text))];
+ }
+
+ // Returns a segment representing an Extended Channel Interpretation
+ // (ECI) designator with the given assignment value.
+ public static makeEci(assignVal: int): QrSegment {
+ const bb: bit[] = [];
+ if (assignVal < 0)
+ throw new RangeError('ECI assignment value out of range');
+ else if (assignVal < 1 << 7) appendBits(assignVal, 8, bb);
+ else if (assignVal < 1 << 14) {
+ appendBits(0b10, 2, bb);
+ appendBits(assignVal, 14, bb);
+ } else if (assignVal < 1000000) {
+ appendBits(0b110, 3, bb);
+ appendBits(assignVal, 21, bb);
+ } else throw new RangeError('ECI assignment value out of range');
+ return new QrSegment(Mode.ECI, 0, bb);
+ }
+
+ // Tests whether the given string can be encoded as a segment in numeric mode.
+ // A string is encodable iff each character is in the range 0 to 9.
+ public static isNumeric(text: string): boolean {
+ return QrSegment.NUMERIC_REGEX.test(text);
+ }
+
+ // Tests whether the given string can be encoded as a segment in alphanumeric mode.
+ // A string is encodable iff each character is in the following set: 0 to 9, A to Z
+ // (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
+ public static isAlphanumeric(text: string): boolean {
+ return QrSegment.ALPHANUMERIC_REGEX.test(text);
+ }
+
+ /*-- Constructor (low level) and fields --*/
+ // The mode indicator of this segment.
+ public mode: Mode;
+
+ // The length of this segment's unencoded data. Measured in characters for
+ // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
+ // Always zero or positive. Not the same as the data's bit length.
+ public numChars: int;
+
+ // The data bits of this segment. Accessed through getData().
+ private bitData: bit[];
+
+ // Creates a new QR Code segment with the given attributes and data.
+ // The character count (numChars) must agree with the mode and the bit buffer length,
+ // but the constraint isn't checked. The given bit buffer is cloned and stored.
+ public constructor(mode: Mode, numChars: int, bitData: bit[]) {
+ this.mode = mode;
+ this.numChars = numChars;
+ this.bitData = bitData;
+ if (numChars < 0) throw new RangeError('Invalid argument');
+ this.bitData = bitData.slice(); // Make defensive copy
+ }
+
+ /*-- Methods --*/
+
+ // Returns a new copy of the data bits of this segment.
+ public getData(): bit[] {
+ return this.bitData.slice(); // Make defensive copy
+ }
+
+ // (Package-private) Calculates and returns the number of bits needed to encode the given segments at
+ // the given version. The result is infinity if a segment has too many characters to fit its length field.
+ public static getTotalBits(
+ segs: Readonly,
+ version: int,
+ ): number {
+ let result: number = 0;
+ for (const seg of segs) {
+ const ccbits: int = seg.mode.numCharCountBits(version);
+ if (seg.numChars >= 1 << ccbits) return Infinity; // The segment's length doesn't fit the field's bit width
+ result += 4 + ccbits + seg.bitData.length;
+ }
+ return result;
+ }
+
+ // Returns a new array of bytes representing the given string encoded in UTF-8.
+ private static toUtf8ByteArray(str: string): byte[] {
+ str = encodeURI(str);
+ const result: byte[] = [];
+ for (let i = 0; i < str.length; i++) {
+ if (str.charAt(i) != '%') result.push(str.charCodeAt(i));
+ else {
+ result.push(parseInt(str.substring(i + 1, i + 3), 16));
+ i += 2;
+ }
+ }
+ return result;
+ }
+
+ /*-- Constants --*/
+
+ // Describes precisely all strings that are encodable in numeric mode.
+ private static readonly NUMERIC_REGEX: RegExp = /^[0-9]*$/;
+
+ // Describes precisely all strings that are encodable in alphanumeric mode.
+ private static readonly ALPHANUMERIC_REGEX: RegExp = /^[A-Z0-9 $%*+.\/:-]*$/;
+
+ // The set of all legal characters in alphanumeric mode,
+ // where each character value maps to the index in the string.
+ private static readonly ALPHANUMERIC_CHARSET: string =
+ '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
+}
+
+/*
+ * A QR Code symbol, which is a type of two-dimension barcode.
+ * Invented by Denso Wave and described in the ISO/IEC 18004 standard.
+ * Instances of this class represent an immutable square grid of dark and light cells.
+ * The class provides static factory functions to create a QR Code from text or binary data.
+ * The class covers the QR Code Model 2 specification, supporting all versions (sizes)
+ * from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
+ *
+ * Ways to create a QR Code object:
+ * - High level: Take the payload data and call QrCode.encodeText() or QrCode.encodeBinary().
+ * - Mid level: Custom-make the list of segments and call QrCode.encodeSegments().
+ * - Low level: Custom-make the array of data codeword bytes (including
+ * segment headers and final padding, excluding error correction codewords),
+ * supply the appropriate version number, and call the QrCode() constructor.
+ * (Note that all ways require supplying the desired error correction level.)
+ */
+export class QrCode {
+ /*-- Static factory functions (high level) --*/
+
+ // Returns a QR Code representing the given Unicode text string at the given error correction level.
+ // As a conservative upper bound, this function is guaranteed to succeed for strings that have 738 or fewer
+ // Unicode code points (not UTF-16 code units) if the low error correction level is used. The smallest possible
+ // QR Code version is automatically chosen for the output. The ECC level of the result may be higher than the
+ // ecl argument if it can be done without increasing the version.
+ public static encodeText(text: string, ecl: Ecc): QrCode {
+ const segs: QrSegment[] = QrSegment.makeSegments(text);
+ return QrCode.encodeSegments(segs, ecl);
+ }
+
+ // Returns a QR Code representing the given binary data at the given error correction level.
+ // This function always encodes using the binary segment mode, not any text mode. The maximum number of
+ // bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output.
+ // The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version.
+ public static encodeBinary(data: Readonly, ecl: Ecc): QrCode {
+ const seg: QrSegment = QrSegment.makeBytes(data);
+ return QrCode.encodeSegments([seg], ecl);
+ }
+
+ /*-- Static factory functions (mid level) --*/
+
+ // Returns a QR Code representing the given segments with the given encoding parameters.
+ // The smallest possible QR Code version within the given range is automatically
+ // chosen for the output. Iff boostEcl is true, then the ECC level of the result
+ // may be higher than the ecl argument if it can be done without increasing the
+ // version. The mask number is either between 0 to 7 (inclusive) to force that
+ // mask, or -1 to automatically choose an appropriate mask (which may be slow).
+ // This function allows the user to create a custom sequence of segments that switches
+ // between modes (such as alphanumeric and byte) to encode text in less space.
+ // This is a mid-level API; the high-level API is encodeText() and encodeBinary().
+ public static encodeSegments(
+ segs: Readonly,
+ ecl: Ecc,
+ minVersion: int = 1,
+ maxVersion: int = 40,
+ mask: int = -1,
+ boostEcl: boolean = true,
+ ): QrCode {
+ if (
+ !(
+ QrCode.MIN_VERSION <= minVersion &&
+ minVersion <= maxVersion &&
+ maxVersion <= QrCode.MAX_VERSION
+ ) ||
+ mask < -1 ||
+ mask > 7
+ )
+ throw new RangeError('Invalid value');
+
+ // Find the minimal version number to use
+ let version: int;
+ let dataUsedBits: int;
+ for (version = minVersion; ; version++) {
+ const dataCapacityBits: int =
+ QrCode.getNumDataCodewords(version, ecl) * 8; // Number of data bits available
+ const usedBits: number = QrSegment.getTotalBits(segs, version);
+ if (usedBits <= dataCapacityBits) {
+ dataUsedBits = usedBits;
+ break; // This version number is found to be suitable
+ }
+ if (version >= maxVersion)
+ // All versions in the range could not fit the given data
+ throw new RangeError('Data too long');
+ }
+
+ // Increase the error correction level while the data still fits in the current version number
+ for (const newEcl of [Ecc.MEDIUM, Ecc.QUARTILE, Ecc.HIGH]) {
+ // From low to high
+ if (
+ boostEcl &&
+ dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8
+ )
+ ecl = newEcl;
+ }
+
+ // Concatenate all segments to create the data bit string
+ const bb: bit[] = [];
+ for (const seg of segs) {
+ appendBits(seg.mode.modeBits, 4, bb);
+ appendBits(seg.numChars, seg.mode.numCharCountBits(version), bb);
+ for (const b of seg.getData()) bb.push(b);
+ }
+ assert(bb.length == dataUsedBits);
+
+ // Add terminator and pad up to a byte if applicable
+ const dataCapacityBits: int = QrCode.getNumDataCodewords(version, ecl) * 8;
+ assert(bb.length <= dataCapacityBits);
+ appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
+ appendBits(0, (8 - (bb.length % 8)) % 8, bb);
+ assert(bb.length % 8 == 0);
+
+ // Pad with alternating bytes until data capacity is reached
+ for (
+ let padByte = 0xec;
+ bb.length < dataCapacityBits;
+ padByte ^= 0xec ^ 0x11
+ )
+ appendBits(padByte, 8, bb);
+
+ // Pack bits into bytes in big endian
+ const dataCodewords: byte[] = [];
+ while (dataCodewords.length * 8 < bb.length) dataCodewords.push(0);
+ bb.forEach(
+ (b: bit, i: int) => (dataCodewords[i >>> 3] |= b << (7 - (i & 7))),
+ );
+
+ // Create the QR Code object
+ return new QrCode(version, ecl, dataCodewords, mask);
+ }
+
+ /*-- Fields --*/
+
+ // The width and height of this QR Code, measured in modules, between
+ // 21 and 177 (inclusive). This is equal to version * 4 + 17.
+ public readonly size: int;
+
+ // The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive).
+ // Even if a QR Code is created with automatic masking requested (mask = -1),
+ // the resulting object still has a mask value between 0 and 7.
+ public readonly mask: int;
+
+ // The modules of this QR Code (false = light, true = dark).
+ // Immutable after constructor finishes. Accessed through getModule().
+ private readonly modules: boolean[][] = [];
+
+ // Indicates function modules that are not subjected to masking. Discarded when constructor finishes.
+ private readonly isFunction: boolean[][] = [];
+
+ /*-- Constructor (low level) and fields --*/
+ // The version number of this QR Code, which is between 1 and 40 (inclusive).
+ // This determines the size of this barcode.
+ public version: int;
+
+ // The error correction level used in this QR Code.
+ public errorCorrectionLevel: Ecc;
+
+ // Creates a new QR Code with the given version number,
+ // error correction level, data codeword bytes, and mask number.
+ // This is a low-level API that most users should not use directly.
+ // A mid-level API is the encodeSegments() function.
+ public constructor(
+ // The version number of this QR Code, which is between 1 and 40 (inclusive).
+ // This determines the size of this barcode.
+ version: int,
+
+ // The error correction level used in this QR Code.
+ errorCorrectionLevel: Ecc,
+
+ dataCodewords: Readonly,
+
+ msk: int,
+ ) {
+ this.version = version;
+ this.errorCorrectionLevel = errorCorrectionLevel;
+ // Check scalar arguments
+ if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION)
+ throw new RangeError('Version value out of range');
+ if (msk < -1 || msk > 7) throw new RangeError('Mask value out of range');
+ this.size = version * 4 + 17;
+
+ // Initialize both grids to be size*size arrays of Boolean false
+ const row: boolean[] = [];
+ for (let i = 0; i < this.size; i++) row.push(false);
+ for (let i = 0; i < this.size; i++) {
+ this.modules.push(row.slice()); // Initially all light
+ this.isFunction.push(row.slice());
+ }
+
+ // Compute ECC, draw modules
+ this.drawFunctionPatterns();
+ const allCodewords: byte[] = this.addEccAndInterleave(dataCodewords);
+ this.drawCodewords(allCodewords);
+
+ // Do masking
+ if (msk == -1) {
+ // Automatically choose best mask
+ let minPenalty: int = 1000000000;
+ for (let i = 0; i < 8; i++) {
+ this.applyMask(i);
+ this.drawFormatBits(i);
+ const penalty: int = this.getPenaltyScore();
+ if (penalty < minPenalty) {
+ msk = i;
+ minPenalty = penalty;
+ }
+ this.applyMask(i); // Undoes the mask due to XOR
+ }
+ }
+ assert(0 <= msk && msk <= 7);
+ this.mask = msk;
+ this.applyMask(msk); // Apply the final choice of mask
+ this.drawFormatBits(msk); // Overwrite old format bits
+
+ this.isFunction = [];
+ }
+
+ /*-- Accessor methods --*/
+
+ // Returns the color of the module (pixel) at the given coordinates, which is false
+ // for light or true for dark. The top left corner has the coordinates (x=0, y=0).
+ // If the given coordinates are out of bounds, then false (light) is returned.
+ public getModule(x: int, y: int): boolean {
+ return (
+ 0 <= x && x < this.size && 0 <= y && y < this.size && this.modules[y][x]
+ );
+ }
+
+ // Modified to expose modules for easy access
+ public getModules() {
+ return this.modules;
+ }
+
+ /*-- Private helper methods for constructor: Drawing function modules --*/
+
+ // Reads this object's version field, and draws and marks all function modules.
+ private drawFunctionPatterns(): void {
+ // Draw horizontal and vertical timing patterns
+ for (let i = 0; i < this.size; i++) {
+ this.setFunctionModule(6, i, i % 2 == 0);
+ this.setFunctionModule(i, 6, i % 2 == 0);
+ }
+
+ // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
+ this.drawFinderPattern(3, 3);
+ this.drawFinderPattern(this.size - 4, 3);
+ this.drawFinderPattern(3, this.size - 4);
+
+ // Draw numerous alignment patterns
+ const alignPatPos: int[] = this.getAlignmentPatternPositions();
+ const numAlign: int = alignPatPos.length;
+ for (let i = 0; i < numAlign; i++) {
+ for (let j = 0; j < numAlign; j++) {
+ // Don't draw on the three finder corners
+ if (
+ !(
+ (i == 0 && j == 0) ||
+ (i == 0 && j == numAlign - 1) ||
+ (i == numAlign - 1 && j == 0)
+ )
+ )
+ this.drawAlignmentPattern(alignPatPos[i], alignPatPos[j]);
+ }
+ }
+
+ // Draw configuration data
+ this.drawFormatBits(0); // Dummy mask value; overwritten later in the constructor
+ this.drawVersion();
+ }
+
+ // Draws two copies of the format bits (with its own error correction code)
+ // based on the given mask and this object's error correction level field.
+ private drawFormatBits(mask: int): void {
+ // Calculate error correction code and pack bits
+ const data: int = (this.errorCorrectionLevel.formatBits << 3) | mask; // errCorrLvl is uint2, mask is uint3
+ let rem: int = data;
+ for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
+ const bits = ((data << 10) | rem) ^ 0x5412; // uint15
+ assert(bits >>> 15 == 0);
+
+ // Draw first copy
+ for (let i = 0; i <= 5; i++) this.setFunctionModule(8, i, getBit(bits, i));
+ this.setFunctionModule(8, 7, getBit(bits, 6));
+ this.setFunctionModule(8, 8, getBit(bits, 7));
+ this.setFunctionModule(7, 8, getBit(bits, 8));
+ for (let i = 9; i < 15; i++)
+ this.setFunctionModule(14 - i, 8, getBit(bits, i));
+
+ // Draw second copy
+ for (let i = 0; i < 8; i++)
+ this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
+ for (let i = 8; i < 15; i++)
+ this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
+ this.setFunctionModule(8, this.size - 8, true); // Always dark
+ }
+
+ // Draws two copies of the version bits (with its own error correction code),
+ // based on this object's version field, iff 7 <= version <= 40.
+ private drawVersion(): void {
+ if (this.version < 7) return;
+
+ // Calculate error correction code and pack bits
+ let rem: int = this.version; // version is uint6, in the range [7, 40]
+ for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
+ const bits: int = (this.version << 12) | rem; // uint18
+ assert(bits >>> 18 == 0);
+
+ // Draw two copies
+ for (let i = 0; i < 18; i++) {
+ const color: boolean = getBit(bits, i);
+ const a: int = this.size - 11 + (i % 3);
+ const b: int = Math.floor(i / 3);
+ this.setFunctionModule(a, b, color);
+ this.setFunctionModule(b, a, color);
+ }
+ }
+
+ // Draws a 9*9 finder pattern including the border separator,
+ // with the center module at (x, y). Modules can be out of bounds.
+ private drawFinderPattern(x: int, y: int): void {
+ for (let dy = -4; dy <= 4; dy++) {
+ for (let dx = -4; dx <= 4; dx++) {
+ const dist: int = Math.max(Math.abs(dx), Math.abs(dy)); // Chebyshev/infinity norm
+ const xx: int = x + dx;
+ const yy: int = y + dy;
+ if (0 <= xx && xx < this.size && 0 <= yy && yy < this.size)
+ this.setFunctionModule(xx, yy, dist != 2 && dist != 4);
+ }
+ }
+ }
+
+ // Draws a 5*5 alignment pattern, with the center module
+ // at (x, y). All modules must be in bounds.
+ private drawAlignmentPattern(x: int, y: int): void {
+ for (let dy = -2; dy <= 2; dy++) {
+ for (let dx = -2; dx <= 2; dx++)
+ this.setFunctionModule(
+ x + dx,
+ y + dy,
+ Math.max(Math.abs(dx), Math.abs(dy)) != 1,
+ );
+ }
+ }
+
+ // Sets the color of a module and marks it as a function module.
+ // Only used by the constructor. Coordinates must be in bounds.
+ private setFunctionModule(x: int, y: int, isDark: boolean): void {
+ this.modules[y][x] = isDark;
+ this.isFunction[y][x] = true;
+ }
+
+ /*-- Private helper methods for constructor: Codewords and masking --*/
+
+ // Returns a new byte string representing the given data with the appropriate error correction
+ // codewords appended to it, based on this object's version and error correction level.
+ private addEccAndInterleave(data: Readonly): byte[] {
+ const ver: int = this.version;
+ const ecl: Ecc = this.errorCorrectionLevel;
+ if (data.length != QrCode.getNumDataCodewords(ver, ecl))
+ throw new RangeError('Invalid argument');
+
+ // Calculate parameter numbers
+ const numBlocks: int = QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver];
+ const blockEccLen: int = QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver];
+ const rawCodewords: int = Math.floor(QrCode.getNumRawDataModules(ver) / 8);
+ const numShortBlocks: int = numBlocks - (rawCodewords % numBlocks);
+ const shortBlockLen: int = Math.floor(rawCodewords / numBlocks);
+
+ // Split data into blocks and append ECC to each block
+ const blocks: byte[][] = [];
+ const rsDiv: byte[] = QrCode.reedSolomonComputeDivisor(blockEccLen);
+ for (let i = 0, k = 0; i < numBlocks; i++) {
+ const dat: byte[] = data.slice(
+ k,
+ k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1),
+ );
+ k += dat.length;
+ const ecc: byte[] = QrCode.reedSolomonComputeRemainder(dat, rsDiv);
+ if (i < numShortBlocks) dat.push(0);
+ blocks.push(dat.concat(ecc));
+ }
+
+ // Interleave (not concatenate) the bytes from every block into a single sequence
+ const result: byte[] = [];
+ for (let i = 0; i < blocks[0].length; i++) {
+ blocks.forEach((block, j) => {
+ // Skip the padding byte in short blocks
+ if (i != shortBlockLen - blockEccLen || j >= numShortBlocks)
+ result.push(block[i]);
+ });
+ }
+ assert(result.length == rawCodewords);
+ return result;
+ }
+
+ // Draws the given sequence of 8-bit codewords (data and error correction) onto the entire
+ // data area of this QR Code. Function modules need to be marked off before this is called.
+ private drawCodewords(data: Readonly): void {
+ if (
+ data.length != Math.floor(QrCode.getNumRawDataModules(this.version) / 8)
+ )
+ throw new RangeError('Invalid argument');
+ let i: int = 0; // Bit index into the data
+ // Do the funny zigzag scan
+ for (let right = this.size - 1; right >= 1; right -= 2) {
+ // Index of right column in each column pair
+ if (right == 6) right = 5;
+ for (let vert = 0; vert < this.size; vert++) {
+ // Vertical counter
+ for (let j = 0; j < 2; j++) {
+ const x: int = right - j; // Actual x coordinate
+ const upward: boolean = ((right + 1) & 2) == 0;
+ const y: int = upward ? this.size - 1 - vert : vert; // Actual y coordinate
+ if (!this.isFunction[y][x] && i < data.length * 8) {
+ this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
+ i++;
+ }
+ // If this QR Code has any remainder bits (0 to 7), they were assigned as
+ // 0/false/light by the constructor and are left unchanged by this method
+ }
+ }
+ }
+ assert(i == data.length * 8);
+ }
+
+ // XORs the codeword modules in this QR Code with the given mask pattern.
+ // The function modules must be marked and the codeword bits must be drawn
+ // before masking. Due to the arithmetic of XOR, calling applyMask() with
+ // the same mask value a second time will undo the mask. A final well-formed
+ // QR Code needs exactly one (not zero, two, etc.) mask applied.
+ private applyMask(mask: int): void {
+ if (mask < 0 || mask > 7) throw new RangeError('Mask value out of range');
+ for (let y = 0; y < this.size; y++) {
+ for (let x = 0; x < this.size; x++) {
+ let invert: boolean;
+ switch (mask) {
+ case 0:
+ invert = (x + y) % 2 == 0;
+ break;
+ case 1:
+ invert = y % 2 == 0;
+ break;
+ case 2:
+ invert = x % 3 == 0;
+ break;
+ case 3:
+ invert = (x + y) % 3 == 0;
+ break;
+ case 4:
+ invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0;
+ break;
+ case 5:
+ invert = ((x * y) % 2) + ((x * y) % 3) == 0;
+ break;
+ case 6:
+ invert = (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;
+ break;
+ case 7:
+ invert = (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;
+ break;
+ default:
+ throw new Error('Unreachable');
+ }
+ if (!this.isFunction[y][x] && invert)
+ this.modules[y][x] = !this.modules[y][x];
+ }
+ }
+ }
+
+ // Calculates and returns the penalty score based on state of this QR Code's current modules.
+ // This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
+ private getPenaltyScore(): int {
+ let result: int = 0;
+
+ // Adjacent modules in row having same color, and finder-like patterns
+ for (let y = 0; y < this.size; y++) {
+ let runColor = false;
+ let runX = 0;
+ const runHistory = [0, 0, 0, 0, 0, 0, 0];
+ for (let x = 0; x < this.size; x++) {
+ if (this.modules[y][x] == runColor) {
+ runX++;
+ if (runX == 5) result += QrCode.PENALTY_N1;
+ else if (runX > 5) result++;
+ } else {
+ this.finderPenaltyAddHistory(runX, runHistory);
+ if (!runColor)
+ result +=
+ this.finderPenaltyCountPatterns(runHistory) * QrCode.PENALTY_N3;
+ runColor = this.modules[y][x];
+ runX = 1;
+ }
+ }
+ result +=
+ this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) *
+ QrCode.PENALTY_N3;
+ }
+ // Adjacent modules in column having same color, and finder-like patterns
+ for (let x = 0; x < this.size; x++) {
+ let runColor = false;
+ let runY = 0;
+ const runHistory = [0, 0, 0, 0, 0, 0, 0];
+ for (let y = 0; y < this.size; y++) {
+ if (this.modules[y][x] == runColor) {
+ runY++;
+ if (runY == 5) result += QrCode.PENALTY_N1;
+ else if (runY > 5) result++;
+ } else {
+ this.finderPenaltyAddHistory(runY, runHistory);
+ if (!runColor)
+ result +=
+ this.finderPenaltyCountPatterns(runHistory) * QrCode.PENALTY_N3;
+ runColor = this.modules[y][x];
+ runY = 1;
+ }
+ }
+ result +=
+ this.finderPenaltyTerminateAndCount(runColor, runY, runHistory) *
+ QrCode.PENALTY_N3;
+ }
+
+ // 2*2 blocks of modules having same color
+ for (let y = 0; y < this.size - 1; y++) {
+ for (let x = 0; x < this.size - 1; x++) {
+ const color: boolean = this.modules[y][x];
+ if (
+ color == this.modules[y][x + 1] &&
+ color == this.modules[y + 1][x] &&
+ color == this.modules[y + 1][x + 1]
+ )
+ result += QrCode.PENALTY_N2;
+ }
+ }
+
+ // Balance of dark and light modules
+ let dark: int = 0;
+ for (const row of this.modules)
+ dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark);
+ const total: int = this.size * this.size; // Note that size is odd, so dark/total != 1/2
+ // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)%
+ const k: int = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
+ assert(0 <= k && k <= 9);
+ result += k * QrCode.PENALTY_N4;
+ assert(0 <= result && result <= 2568888); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4
+ return result;
+ }
+
+ /*-- Private helper functions --*/
+
+ // Returns an ascending list of positions of alignment patterns for this version number.
+ // Each position is in the range [0,177), and are used on both the x and y axes.
+ // This could be implemented as lookup table of 40 variable-length lists of integers.
+ private getAlignmentPatternPositions(): int[] {
+ if (this.version == 1) return [];
+ else {
+ const numAlign: int = Math.floor(this.version / 7) + 2;
+ const step: int =
+ this.version == 32
+ ? 26
+ : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
+ const result: int[] = [6];
+ for (let pos = this.size - 7; result.length < numAlign; pos -= step)
+ result.splice(1, 0, pos);
+ return result;
+ }
+ }
+
+ // Returns the number of data bits that can be stored in a QR Code of the given version number, after
+ // all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
+ // The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
+ private static getNumRawDataModules(ver: int): int {
+ if (ver < QrCode.MIN_VERSION || ver > QrCode.MAX_VERSION)
+ throw new RangeError('Version number out of range');
+ let result: int = (16 * ver + 128) * ver + 64;
+ if (ver >= 2) {
+ const numAlign: int = Math.floor(ver / 7) + 2;
+ result -= (25 * numAlign - 10) * numAlign - 55;
+ if (ver >= 7) result -= 36;
+ }
+ assert(208 <= result && result <= 29648);
+ return result;
+ }
+
+ // Returns the number of 8-bit data (i.e. not error correction) codewords contained in any
+ // QR Code of the given version number and error correction level, with remainder bits discarded.
+ // This stateless pure function could be implemented as a (40*4)-cell lookup table.
+ private static getNumDataCodewords(ver: int, ecl: Ecc): int {
+ return (
+ Math.floor(QrCode.getNumRawDataModules(ver) / 8) -
+ QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver] *
+ QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver]
+ );
+ }
+
+ // Returns a Reed-Solomon ECC generator polynomial for the given degree. This could be
+ // implemented as a lookup table over all possible parameter values, instead of as an algorithm.
+ private static reedSolomonComputeDivisor(degree: int): byte[] {
+ if (degree < 1 || degree > 255) throw new RangeError('Degree out of range');
+ // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.
+ // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93].
+ const result: byte[] = [];
+ for (let i = 0; i < degree - 1; i++) result.push(0);
+ result.push(1); // Start off with the monomial x^0
+
+ // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
+ // and drop the highest monomial term which is always 1x^degree.
+ // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
+ let root = 1;
+ for (let i = 0; i < degree; i++) {
+ // Multiply the current product by (x - r^i)
+ for (let j = 0; j < result.length; j++) {
+ result[j] = QrCode.reedSolomonMultiply(result[j], root);
+ if (j + 1 < result.length) result[j] ^= result[j + 1];
+ }
+ root = QrCode.reedSolomonMultiply(root, 0x02);
+ }
+ return result;
+ }
+
+ // Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials.
+ private static reedSolomonComputeRemainder(
+ data: Readonly,
+ divisor: Readonly,
+ ): byte[] {
+ const result: byte[] = divisor.map(() => 0);
+ for (const b of data) {
+ // Polynomial division
+ const factor: byte = b ^ (result.shift() as byte);
+ result.push(0);
+ divisor.forEach(
+ (coef, i) => (result[i] ^= QrCode.reedSolomonMultiply(coef, factor)),
+ );
+ }
+ return result;
+ }
+
+ // Returns the product of the two given field elements modulo GF(2^8/0x11D). The arguments and result
+ // are unsigned 8-bit integers. This could be implemented as a lookup table of 256*256 entries of uint8.
+ private static reedSolomonMultiply(x: byte, y: byte): byte {
+ if (x >>> 8 != 0 || y >>> 8 != 0) throw new RangeError('Byte out of range');
+ // Russian peasant multiplication
+ let z: int = 0;
+ for (let i = 7; i >= 0; i--) {
+ z = (z << 1) ^ ((z >>> 7) * 0x11d);
+ z ^= ((y >>> i) & 1) * x;
+ }
+ assert(z >>> 8 == 0);
+ return z as byte;
+ }
+
+ // Can only be called immediately after a light run is added, and
+ // returns either 0, 1, or 2. A helper function for getPenaltyScore().
+ private finderPenaltyCountPatterns(runHistory: Readonly): int {
+ const n: int = runHistory[1];
+ assert(n <= this.size * 3);
+ const core: boolean =
+ n > 0 &&
+ runHistory[2] == n &&
+ runHistory[3] == n * 3 &&
+ runHistory[4] == n &&
+ runHistory[5] == n;
+ return (
+ (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) +
+ (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0)
+ );
+ }
+
+ // Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
+ private finderPenaltyTerminateAndCount(
+ currentRunColor: boolean,
+ currentRunLength: int,
+ runHistory: int[],
+ ): int {
+ if (currentRunColor) {
+ // Terminate dark run
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
+ currentRunLength = 0;
+ }
+ currentRunLength += this.size; // Add light border to final run
+ this.finderPenaltyAddHistory(currentRunLength, runHistory);
+ return this.finderPenaltyCountPatterns(runHistory);
+ }
+
+ // Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
+ private finderPenaltyAddHistory(
+ currentRunLength: int,
+ runHistory: int[],
+ ): void {
+ if (runHistory[0] == 0) currentRunLength += this.size; // Add light border to initial run
+ runHistory.pop();
+ runHistory.unshift(currentRunLength);
+ }
+
+ /*-- Constants and tables --*/
+
+ // The minimum version number supported in the QR Code Model 2 standard.
+ public static readonly MIN_VERSION: int = 1;
+ // The maximum version number supported in the QR Code Model 2 standard.
+ public static readonly MAX_VERSION: int = 40;
+
+ // For use in getPenaltyScore(), when evaluating which mask is best.
+ private static readonly PENALTY_N1: int = 3;
+ private static readonly PENALTY_N2: int = 3;
+ private static readonly PENALTY_N3: int = 40;
+ private static readonly PENALTY_N4: int = 10;
+
+ private static readonly ECC_CODEWORDS_PER_BLOCK: int[][] = [
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
+ //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
+ [
+ -1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30,
+ 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,
+ 30, 30, 30, 30,
+ ], // Low
+ [
+ -1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28,
+ 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+ 28, 28, 28, 28, 28,
+ ], // Medium
+ [
+ -1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28,
+ 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30,
+ 30, 30, 30, 30, 30,
+ ], // Quartile
+ [
+ -1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28,
+ 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30,
+ 30, 30, 30, 30, 30,
+ ], // High
+ ];
+
+ private static readonly NUM_ERROR_CORRECTION_BLOCKS: int[][] = [
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
+ //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
+ [
+ -1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9,
+ 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25,
+ ], // Low
+ [
+ -1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17,
+ 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47,
+ 49,
+ ], // Medium
+ [
+ -1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20,
+ 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62,
+ 65, 68,
+ ], // Quartile
+ [
+ -1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25,
+ 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74,
+ 77, 81,
+ ], // High
+ ];
+}
diff --git a/src/mock.tsx b/src/mock.tsx
deleted file mode 100644
index d5d088a..0000000
--- a/src/mock.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as React from 'react';
-import { generateTrigger } from './index';
-
-interface MockPortalProps {
- open?: boolean;
- autoDestroy?: boolean;
- children: React.ReactElement;
- getContainer?: () => HTMLElement;
-}
-
-const MockPortal: React.FC = ({
- open,
- autoDestroy,
- children,
- getContainer,
-}) => {
- const [visible, setVisible] = React.useState(open);
-
- React.useEffect(() => {
- getContainer?.();
- });
-
- React.useEffect(() => {
- if (open) {
- setVisible(true);
- } else if (autoDestroy) {
- setVisible(false);
- }
- }, [open, autoDestroy]);
-
- return visible ? children : null;
-};
-
-export default generateTrigger(MockPortal);
diff --git a/src/util.ts b/src/util.ts
deleted file mode 100644
index 5170d86..0000000
--- a/src/util.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-import type { CSSMotionProps } from 'rc-motion';
-import type {
- AlignType,
- AnimationType,
- BuildInPlacements,
- TransitionNameType,
-} from './interface';
-
-function isPointsEq(
- a1: string[] = [],
- a2: string[] = [],
- isAlignPoint: boolean,
-): boolean {
- if (isAlignPoint) {
- return a1[0] === a2[0];
- }
- return a1[0] === a2[0] && a1[1] === a2[1];
-}
-
-export function getAlignPopupClassName(
- builtinPlacements: BuildInPlacements,
- prefixCls: string,
- align: AlignType,
- isAlignPoint: boolean,
-): string {
- const { points } = align;
-
- const placements = Object.keys(builtinPlacements);
-
- for (let i = 0; i < placements.length; i += 1) {
- const placement = placements[i];
- if (
- isPointsEq(builtinPlacements[placement]?.points, points, isAlignPoint)
- ) {
- return `${prefixCls}-placement-${placement}`;
- }
- }
-
- return '';
-}
-
-/** @deprecated We should not use this if we can refactor all deps */
-export function getMotion(
- prefixCls: string,
- motion: CSSMotionProps,
- animation: AnimationType,
- transitionName: TransitionNameType,
-): CSSMotionProps {
- if (motion) {
- return motion;
- }
-
- if (animation) {
- return {
- motionName: `${prefixCls}-${animation}`,
- };
- }
-
- if (transitionName) {
- return {
- motionName: transitionName,
- };
- }
-
- return null;
-}
-
-export function getWin(ele: HTMLElement) {
- return ele.ownerDocument.defaultView;
-}
-
-/**
- * Get all the scrollable parent elements of the element
- * @param ele The element to be detected
- * @param areaOnly Only return the parent which will cut visible area
- */
-export function collectScroller(ele: HTMLElement) {
- const scrollerList: HTMLElement[] = [];
- let current = ele?.parentElement;
-
- const scrollStyle = ['hidden', 'scroll', 'clip', 'auto'];
-
- while (current) {
- const { overflowX, overflowY, overflow } =
- getWin(current).getComputedStyle(current);
- if ([overflowX, overflowY, overflow].some((o) => scrollStyle.includes(o))) {
- scrollerList.push(current);
- }
-
- current = current.parentElement;
- }
-
- return scrollerList;
-}
-
-export function toNum(num: number, defaultValue = 1) {
- return Number.isNaN(num) ? defaultValue : num;
-}
-
-function getPxValue(val: string) {
- return toNum(parseFloat(val), 0);
-}
-
-export interface VisibleArea {
- left: number;
- top: number;
- right: number;
- bottom: number;
-}
-
-/**
- *
- *
- * **************************************
- * * Border *
- * * ************************** *
- * * * * * *
- * * B * * S * B *
- * * o * * c * o *
- * * r * Content * r * r *
- * * d * * o * d *
- * * e * * l * e *
- * * r ******************** l * r *
- * * * Scroll * *
- * * ************************** *
- * * Border *
- * **************************************
- *
- */
-/**
- * Get visible area of element
- */
-export function getVisibleArea(
- initArea: VisibleArea,
- scrollerList?: HTMLElement[],
-) {
- const visibleArea = { ...initArea };
-
- (scrollerList || []).forEach((ele) => {
- if (ele instanceof HTMLBodyElement || ele instanceof HTMLHtmlElement) {
- return;
- }
-
- // Skip if static position which will not affect visible area
- const {
- overflow,
- overflowClipMargin,
- borderTopWidth,
- borderBottomWidth,
- borderLeftWidth,
- borderRightWidth,
- } = getWin(ele).getComputedStyle(ele);
-
- const eleRect = ele.getBoundingClientRect();
- const {
- offsetHeight: eleOutHeight,
- clientHeight: eleInnerHeight,
- offsetWidth: eleOutWidth,
- clientWidth: eleInnerWidth,
- } = ele;
-
- const borderTopNum = getPxValue(borderTopWidth);
- const borderBottomNum = getPxValue(borderBottomWidth);
- const borderLeftNum = getPxValue(borderLeftWidth);
- const borderRightNum = getPxValue(borderRightWidth);
-
- const scaleX = toNum(
- Math.round((eleRect.width / eleOutWidth) * 1000) / 1000,
- );
- const scaleY = toNum(
- Math.round((eleRect.height / eleOutHeight) * 1000) / 1000,
- );
-
- // Original visible area
- const eleScrollWidth =
- (eleOutWidth - eleInnerWidth - borderLeftNum - borderRightNum) * scaleX;
- const eleScrollHeight =
- (eleOutHeight - eleInnerHeight - borderTopNum - borderBottomNum) * scaleY;
-
- // Cut border size
- const scaledBorderTopWidth = borderTopNum * scaleY;
- const scaledBorderBottomWidth = borderBottomNum * scaleY;
- const scaledBorderLeftWidth = borderLeftNum * scaleX;
- const scaledBorderRightWidth = borderRightNum * scaleX;
-
- // Clip margin
- let clipMarginWidth = 0;
- let clipMarginHeight = 0;
- if (overflow === 'clip') {
- const clipNum = getPxValue(overflowClipMargin);
- clipMarginWidth = clipNum * scaleX;
- clipMarginHeight = clipNum * scaleY;
- }
-
- // Region
- const eleLeft = eleRect.x + scaledBorderLeftWidth - clipMarginWidth;
- const eleTop = eleRect.y + scaledBorderTopWidth - clipMarginHeight;
-
- const eleRight =
- eleLeft +
- eleRect.width +
- 2 * clipMarginWidth -
- scaledBorderLeftWidth -
- scaledBorderRightWidth -
- eleScrollWidth;
-
- const eleBottom =
- eleTop +
- eleRect.height +
- 2 * clipMarginHeight -
- scaledBorderTopWidth -
- scaledBorderBottomWidth -
- eleScrollHeight;
-
- visibleArea.left = Math.max(visibleArea.left, eleLeft);
- visibleArea.top = Math.max(visibleArea.top, eleTop);
- visibleArea.right = Math.min(visibleArea.right, eleRight);
- visibleArea.bottom = Math.min(visibleArea.bottom, eleBottom);
- });
-
- return visibleArea;
-}
diff --git a/src/utils.tsx b/src/utils.tsx
new file mode 100644
index 0000000..b3c80ca
--- /dev/null
+++ b/src/utils.tsx
@@ -0,0 +1,160 @@
+import type {
+ CrossOrigin,
+ ERROR_LEVEL_MAPPED_TYPE,
+ ErrorCorrectionLevel,
+ Excavation,
+ ImageSettings,
+ Modules,
+} from './interface';
+import { Ecc } from './libs/qrcodegen';
+
+export const ERROR_LEVEL_MAP: ERROR_LEVEL_MAPPED_TYPE = {
+ L: Ecc.LOW,
+ M: Ecc.MEDIUM,
+ Q: Ecc.QUARTILE,
+ H: Ecc.HIGH,
+} as const;
+
+export const DEFAULT_SIZE = 128;
+export const DEFAULT_LEVEL: ErrorCorrectionLevel = 'L';
+export const DEFAULT_BGCOLOR = '#FFFFFF';
+export const DEFAULT_FGCOLOR = '#000000';
+export const DEFAULT_INCLUDEMARGIN = false;
+export const DEFAULT_MINVERSION = 1;
+
+export const SPEC_MARGIN_SIZE = 4;
+export const DEFAULT_MARGIN_SIZE = 0;
+
+// This is *very* rough estimate of max amount of QRCode allowed to be covered.
+// It is "wrong" in a lot of ways (area is a terrible way to estimate, it
+// really should be number of modules covered), but if for some reason we don't
+// get an explicit height or width, I'd rather default to something than throw.
+export const DEFAULT_IMG_SCALE = 0.1;
+
+export function generatePath(modules: Modules, margin: number = 0): string {
+ const ops: string[] = [];
+ modules.forEach(function (row, y) {
+ let start: number | null = null;
+ row.forEach(function (cell, x) {
+ if (!cell && start !== null) {
+ // M0 0h7v1H0z injects the space with the move and drops the comma,
+ // saving a char per operation
+ ops.push(
+ `M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z`,
+ );
+ start = null;
+ return;
+ }
+
+ // end of row, clean up or skip
+ if (x === row.length - 1) {
+ if (!cell) {
+ // We would have closed the op above already so this can only mean
+ // 2+ light modules in a row.
+ return;
+ }
+ if (start === null) {
+ // Just a single dark module.
+ ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`);
+ } else {
+ // Otherwise finish the current line.
+ ops.push(
+ `M${start + margin},${y + margin} h${x + 1 - start}v1H${
+ start + margin
+ }z`,
+ );
+ }
+ return;
+ }
+
+ if (cell && start === null) {
+ start = x;
+ }
+ });
+ });
+ return ops.join('');
+}
+
+// We could just do this in generatePath, except that we want to support
+// non-Path2D canvas, so we need to keep it an explicit step.
+export function excavateModules(modules: Modules, excavation: Excavation): Modules {
+ return modules.slice().map((row, y) => {
+ if (y < excavation.y || y >= excavation.y + excavation.h) {
+ return row;
+ }
+ return row.map((cell, x) => {
+ if (x < excavation.x || x >= excavation.x + excavation.w) {
+ return cell;
+ }
+ return false;
+ });
+ });
+}
+
+export function getImageSettings(
+ cells: Modules,
+ size: number,
+ margin: number,
+ imageSettings?: ImageSettings,
+): null | {
+ x: number;
+ y: number;
+ h: number;
+ w: number;
+ excavation: Excavation | null;
+ opacity: number;
+ crossOrigin: CrossOrigin;
+} {
+ if (imageSettings == null) {
+ return null;
+ }
+ const numCells = cells.length + margin * 2;
+ const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE);
+ const scale = numCells / size;
+ const w = (imageSettings.width || defaultSize) * scale;
+ const h = (imageSettings.height || defaultSize) * scale;
+ const x =
+ imageSettings.x == null
+ ? cells.length / 2 - w / 2
+ : imageSettings.x * scale;
+ const y =
+ imageSettings.y == null
+ ? cells.length / 2 - h / 2
+ : imageSettings.y * scale;
+ const opacity = imageSettings.opacity == null ? 1 : imageSettings.opacity;
+
+ let excavation = null;
+ if (imageSettings.excavate) {
+ const floorX = Math.floor(x);
+ const floorY = Math.floor(y);
+ const ceilW = Math.ceil(w + x - floorX);
+ const ceilH = Math.ceil(h + y - floorY);
+ excavation = { x: floorX, y: floorY, w: ceilW, h: ceilH };
+ }
+
+ const crossOrigin = imageSettings.crossOrigin;
+
+ return { x, y, h, w, excavation, opacity, crossOrigin };
+}
+
+export function getMarginSize(includeMargin: boolean, marginSize?: number): number {
+ if (marginSize != null) {
+ return Math.floor(marginSize);
+ }
+ return includeMargin ? SPEC_MARGIN_SIZE : DEFAULT_MARGIN_SIZE;
+}
+
+
+// For canvas we're going to switch our drawing mode based on whether or not
+// the environment supports Path2D. We only need the constructor to be
+// supported, but Edge doesn't actually support the path (string) type
+// argument. Luckily it also doesn't support the addPath() method. We can
+// treat that as the same thing.
+export const SUPPORTS_PATH2D = (function () {
+ try {
+ new Path2D().addPath(new Path2D());
+ } catch (e) {
+ return false;
+ }
+ return true;
+})();
diff --git a/tests/__snapshots__/mobile.test.tsx.snap b/tests/__snapshots__/mobile.test.tsx.snap
deleted file mode 100644
index 32c0894..0000000
--- a/tests/__snapshots__/mobile.test.tsx.snap
+++ /dev/null
@@ -1,14 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Trigger.Mobile popupRender 1`] = `
-
-`;
diff --git a/tests/_utils.ts b/tests/_utils.ts
new file mode 100644
index 0000000..7d09ea6
--- /dev/null
+++ b/tests/_utils.ts
@@ -0,0 +1,22 @@
+import { act } from '@testing-library/react';
+
+/**
+ * Wait for a time delay. Will wait `advanceTime * times` ms.
+ *
+ * @param advanceTime Default 1000
+ * @param times Default 20
+ */
+export async function waitFakeTimer(advanceTime = 1000, times = 20) {
+ for (let i = 0; i < times; i += 1) {
+ // eslint-disable-next-line no-await-in-loop
+ await act(async () => {
+ await Promise.resolve();
+
+ if (advanceTime > 0) {
+ jest.advanceTimersByTime(advanceTime);
+ } else {
+ jest.runAllTimers();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/tests/align.test.tsx b/tests/align.test.tsx
deleted file mode 100644
index 9366cc7..0000000
--- a/tests/align.test.tsx
+++ /dev/null
@@ -1,297 +0,0 @@
-import { act, cleanup, fireEvent, render } from '@testing-library/react';
-import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
-import React from 'react';
-import type { TriggerProps, TriggerRef } from '../src';
-import Trigger from '../src';
-import { awaitFakeTimer } from './util';
-
-import { _rs } from 'rc-resize-observer';
-
-export const triggerResize = (target: Element) => {
- act(() => {
- _rs([{ target } as ResizeObserverEntry]);
- });
-};
-
-describe('Trigger.Align', () => {
- let targetVisible = true;
-
- let rectX = 100;
- let rectY = 100;
- let rectWidth = 100;
- let rectHeight = 100;
-
- beforeAll(() => {
- spyElementPrototypes(HTMLDivElement, {
- getBoundingClientRect: () => ({
- x: rectX,
- y: rectY,
- width: rectWidth,
- height: rectHeight,
- right: 200,
- bottom: 200,
- }),
- });
-
- spyElementPrototypes(HTMLElement, {
- offsetParent: {
- get: () => (targetVisible ? document.body : null),
- },
- });
- spyElementPrototypes(SVGElement, {
- offsetParent: {
- get: () => (targetVisible ? document.body : null),
- },
- });
- });
-
- beforeEach(() => {
- targetVisible = true;
-
- rectX = 100;
- rectY = 100;
- rectWidth = 100;
- rectHeight = 100;
-
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- cleanup();
- jest.useRealTimers();
- });
-
- it('not show', async () => {
- const onAlign = jest.fn();
-
- const Demo = (props: Partial) => {
- const scrollRef = React.useRef(null);
-
- return (
- <>
-
- trigger}
- getPopupContainer={() => scrollRef.current!}
- {...props}
- >
-
-
- >
- );
- };
-
- const { rerender, container } = render();
- const scrollDiv = container.querySelector('.scroll')!;
-
- const mockAddEvent = jest.spyOn(scrollDiv, 'addEventListener');
-
- expect(mockAddEvent).not.toHaveBeenCalled();
-
- // Visible
- rerender();
- expect(mockAddEvent).toHaveBeenCalled();
-
- // Scroll
- onAlign.mockReset();
- fireEvent.scroll(scrollDiv);
-
- await awaitFakeTimer();
- expect(onAlign).toHaveBeenCalled();
- });
-
- it('resize align', async () => {
- const onAlign = jest.fn();
-
- const { container } = render(
- trigger}
- >
-
- ,
- );
-
- await Promise.resolve();
- onAlign.mockReset();
-
- // Resize
- const target = container.querySelector('.target')!;
- triggerResize(target);
-
- await awaitFakeTimer();
- expect(onAlign).toHaveBeenCalled();
- });
-
- it('placement is higher than popupAlign', async () => {
- render(
- }
- builtinPlacements={{
- top: {},
- }}
- popupPlacement="top"
- popupAlign={{}}
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- expect(
- document.querySelector('.rc-trigger-popup-placement-top'),
- ).toBeTruthy();
- });
-
- it('invisible should not align', async () => {
- const onPopupAlign = jest.fn();
- const triggerRef = React.createRef();
-
- render(
- }
- popupAlign={{}}
- onPopupAlign={onPopupAlign}
- ref={triggerRef}
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- expect(onPopupAlign).toHaveBeenCalled();
- onPopupAlign.mockReset();
-
- for (let i = 0; i < 10; i += 1) {
- triggerRef.current!.forceAlign();
-
- await awaitFakeTimer();
- expect(onPopupAlign).toHaveBeenCalled();
- onPopupAlign.mockReset();
- }
-
- // Make invisible
- targetVisible = false;
-
- triggerRef.current!.forceAlign();
- await awaitFakeTimer();
- expect(onPopupAlign).not.toHaveBeenCalled();
- });
-
- it('align should merge into placement', async () => {
- render(
- }
- builtinPlacements={{
- top: {
- targetOffset: [0, 0],
- },
- }}
- popupPlacement="top"
- popupAlign={{
- targetOffset: [-903, -1128],
- }}
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- expect(
- document.querySelector('.rc-trigger-popup-placement-top'),
- ).toHaveStyle({
- left: `753px`,
- top: `978px`,
- });
- });
-
- it('targetOffset support ptg', async () => {
- render(
- }
- popupAlign={{
- targetOffset: ['50%', '-50%'],
- }}
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- // Correct this if I miss understand the value calculation
- // from https://github.com/yiminghe/dom-align/blob/master/src/getElFuturePos.js
- expect(document.querySelector('.rc-trigger-popup')).toHaveStyle({
- left: `-50px`,
- top: `50px`,
- });
- });
-
- it('support dynamicInset', async () => {
- render(
- }
- popupAlign={{
- points: ['bc', 'tc'],
- _experimental: {
- dynamicInset: true,
- },
- }}
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- expect(document.querySelector('.rc-trigger-popup')).toHaveStyle({
- bottom: `100px`,
- });
- });
-
- it('round when decimal precision', async () => {
- rectX = 22.6;
- rectY = 33.4;
- rectWidth = 33.7;
- rectHeight = 55.9;
-
- render(
- }
- popupAlign={{
- points: ['tl', 'bl'],
- }}
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- expect(document.querySelector('.rc-trigger-popup')).toHaveStyle({
- top: `56px`,
- });
- });
-});
diff --git a/tests/arrow.test.jsx b/tests/arrow.test.jsx
deleted file mode 100644
index 80be36a..0000000
--- a/tests/arrow.test.jsx
+++ /dev/null
@@ -1,212 +0,0 @@
-/* eslint-disable max-classes-per-file */
-
-import { act, cleanup, render } from '@testing-library/react';
-import {
- spyElementPrototype,
- spyElementPrototypes,
-} from 'rc-util/lib/test/domHook';
-import Trigger from '../src';
-
-describe('Trigger.Arrow', () => {
- beforeAll(() => {
- spyElementPrototypes(HTMLElement, {
- offsetParent: {
- get: () => document.body,
- },
- });
- });
-
- beforeEach(() => {
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- cleanup();
- jest.useRealTimers();
- });
-
- async function awaitFakeTimer() {
- for (let i = 0; i < 10; i += 1) {
- await act(async () => {
- jest.advanceTimersByTime(100);
- await Promise.resolve();
- });
- }
- }
-
- it('not show', () => {
- render(
- trigger} arrow>
-
- ,
- );
- });
-
- describe('direction', () => {
- let divSpy;
- let windowSpy;
-
- beforeAll(() => {
- divSpy = spyElementPrototype(
- HTMLDivElement,
- 'getBoundingClientRect',
- () => ({
- x: 200,
- y: 200,
- width: 100,
- height: 50,
- }),
- );
-
- windowSpy = spyElementPrototypes(Window, {
- clientWidth: {
- get: () => 1000,
- },
- clientHeight: {
- get: () => 1000,
- },
- });
- });
-
- afterAll(() => {
- divSpy.mockRestore();
- windowSpy.mockRestore();
- });
-
- function test(name, align, style) {
- it(name, async () => {
- render(
- trigger}
- arrow
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- expect(document.querySelector('.rc-trigger-popup-arrow')).toHaveStyle(
- style,
- );
- });
- }
-
- // Top
- test(
- 'top',
- {
- points: ['bl', 'tl'],
- },
- {
- bottom: 0,
- },
- );
-
- // Bottom
- test(
- 'bottom',
- {
- points: ['tc', 'bc'],
- },
- {
- top: 0,
- },
- );
-
- // Left
- test(
- 'left',
- {
- points: ['cr', 'cl'],
- },
- {
- right: 0,
- },
- );
-
- // Right
- test(
- 'right',
- {
- points: ['cl', 'cr'],
- },
- {
- left: 0,
- },
- );
-
- it('not aligned', async () => {
- render(
- trigger}
- arrow
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- // Not have other align style
- const { style } = document.querySelector('.rc-trigger-popup-arrow');
- expect(style.position).toBeTruthy();
- expect(style.left).toBeFalsy();
- expect(style.right).toBeFalsy();
- expect(style.top).toBeFalsy();
- expect(style.bottom).toBeFalsy();
- });
-
- it('arrow classname', async () => {
- render(
- trigger}
- arrow={{
- className: 'abc',
- }}
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- const arrowDom = document.querySelector('.rc-trigger-popup-arrow');
- expect(arrowDom.classList.contains('abc')).toBeTruthy();
- });
- });
-
- it('content', async () => {
- render(
- trigger}
- arrow={{
- content: ,
- }}
- >
-
- ,
- );
-
- await awaitFakeTimer();
-
- expect(document.querySelector('.my-content')).toBeTruthy();
- });
-});
diff --git a/tests/basic.test.jsx b/tests/basic.test.jsx
deleted file mode 100644
index aa4e87d..0000000
--- a/tests/basic.test.jsx
+++ /dev/null
@@ -1,1201 +0,0 @@
-/* eslint-disable max-classes-per-file */
-
-import { act, cleanup, fireEvent, render } from '@testing-library/react';
-import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
-import React, { StrictMode, createRef } from 'react';
-import ReactDOM from 'react-dom';
-import Trigger from '../src';
-import { awaitFakeTimer, placementAlignMap } from './util';
-
-describe('Trigger.Basic', () => {
- beforeAll(() => {
- spyElementPrototypes(HTMLElement, {
- offsetParent: {
- get: () => document.body,
- },
- });
- });
-
- beforeEach(() => {
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- cleanup();
- jest.useRealTimers();
- });
-
- function trigger(dom, selector, method = 'click') {
- fireEvent[method](dom.querySelector(selector));
- act(() => jest.runAllTimers());
- }
-
- function isPopupHidden() {
- return document
- .querySelector('.rc-trigger-popup')
- .className.includes('-hidden');
- }
- function isPopupClassHidden(name) {
- return document.querySelector(name).className.includes('-hidden');
- }
- function isPopupAllHidden() {
- const popupArr = document.querySelectorAll('.rc-trigger-popup');
-
- return Array.from(popupArr).every((item) =>
- item.className.includes('-hidden'),
- );
- }
- describe('getPopupContainer', () => {
- it('defaults to document.body', () => {
- const { container } = render(
- tooltip2}
- >
- click
- ,
- );
-
- trigger(container, '.target');
-
- const popupDomNode = document.querySelector('.rc-trigger-popup');
- expect(popupDomNode.parentNode.parentNode).toBeInstanceOf(
- HTMLBodyElement,
- );
- });
-
- it('can change', () => {
- function getPopupContainer(node) {
- return node.parentNode;
- }
-
- const { container } = render(
-
-
tooltip2}
- >
- click
-
-
,
- document.createElement('div'),
- );
-
- trigger(container, '.target');
-
- const popupDomNode = document.querySelector('.rc-trigger-popup');
- expect(popupDomNode.parentNode).toBe(container.querySelector('.holder'));
- });
- });
-
- describe('action', () => {
- it('click works', () => {
- const { container } = render(
- tooltip2}
- >
- click
- ,
- );
-
- trigger(container, '.target');
- expect(document.querySelector('.x-content').textContent).toBe('tooltip2');
-
- trigger(container, '.target');
- expect(isPopupHidden).toBeTruthy();
- });
-
- it('click works with function', () => {
- const popup = function renderPopup() {
- return tooltip3;
- };
- const { container } = render(
-
- click
- ,
- );
-
- trigger(container, '.target');
- expect(document.querySelector('.x-content').textContent).toBe('tooltip3');
-
- trigger(container, '.target');
- expect(isPopupHidden()).toBeTruthy();
- });
-
- describe('hover works', () => {
- it('mouse event', () => {
- const { container } = render(
- trigger}
- >
- click
- ,
- );
-
- trigger(container, '.target', 'mouseEnter');
- expect(isPopupHidden()).toBeFalsy();
-
- trigger(container, '.target', 'mouseLeave');
- expect(isPopupHidden()).toBeTruthy();
- });
-
- it('pointer event', () => {
- const { container } = render(
- trigger}
- >
- click
- ,
- );
-
- trigger(container, '.target', 'pointerEnter');
- expect(isPopupHidden()).toBeFalsy();
-
- trigger(container, '.target', 'pointerLeave');
- expect(isPopupHidden()).toBeTruthy();
-
- // Enter again but move in popup
- trigger(container, '.target', 'pointerEnter');
- expect(isPopupHidden()).toBeFalsy();
-
- fireEvent.pointerLeave(container.querySelector('.target'));
- trigger(document, '.rc-trigger-popup', 'pointerEnter');
- expect(isPopupHidden()).toBeFalsy();
- });
- });
-
- it('contextMenu works', () => {
- const triggerRef = createRef();
- const { container } = render(
- trigger}
- >
- contextMenu
- ,
- );
-
- trigger(container, '.target', 'contextMenu');
- expect(isPopupHidden()).toBeFalsy();
-
- fireEvent.click(document.querySelector('.target'));
-
- expect(isPopupHidden()).toBeTruthy();
- });
- it('contextMenu all close', () => {
- const triggerRef1 = createRef();
- const triggerRef2 = createRef();
- const { container } = render(
- <>
- trigger1}
- >
- contextMenu 1
-
- trigger2}
- >
- contextMenu 2
-
- >,
- );
-
- trigger(container, '.target1', 'contextMenu');
- trigger(container, '.target2', 'contextMenu');
- expect(isPopupClassHidden('.trigger-popup1')).toBeTruthy();
- expect(isPopupClassHidden('.trigger-popup2')).toBeFalsy();
-
- trigger(container, '.target1', 'contextMenu');
- expect(isPopupClassHidden('.trigger-popup1')).toBeFalsy();
- expect(isPopupClassHidden('.trigger-popup2')).toBeTruthy();
-
- fireEvent.mouseDown(document.body);
- expect(isPopupAllHidden()).toBeTruthy();
- });
- describe('afterPopupVisibleChange can be triggered', () => {
- it('uncontrolled', async () => {
- let triggered = 0;
- const { container } = render(
- {
- triggered = 1;
- }}
- popup={trigger}
- >
- click
- ,
- );
-
- trigger(container, '.target');
-
- await awaitFakeTimer();
-
- expect(triggered).toBe(1);
- });
-
- it('controlled', async () => {
- let triggered = 0;
-
- const Demo = () => {
- const [visible, setVisible] = React.useState(false);
-
- return (
- <>
-