fix: button wave not work
parent
7d1418d9f4
commit
dcc3bb10cb
|
@ -1,178 +0,0 @@
|
|||
import { nextTick, defineComponent, getCurrentInstance, onMounted, onBeforeUnmount } from 'vue';
|
||||
import TransitionEvents from './css-animation/Event';
|
||||
import raf from './raf';
|
||||
import { findDOMNode } from './props-util';
|
||||
import useConfigInject from '../config-provider/hooks/useConfigInject';
|
||||
let styleForPesudo: HTMLStyleElement;
|
||||
|
||||
// Where el is the DOM element you'd like to test for visibility
|
||||
function isHidden(element: HTMLElement) {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return false;
|
||||
}
|
||||
return !element || element.offsetParent === null;
|
||||
}
|
||||
function isNotGrey(color: string) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\.\d]*)?\)/);
|
||||
if (match && match[1] && match[2] && match[3]) {
|
||||
return !(match[1] === match[2] && match[2] === match[3]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'Wave',
|
||||
props: {
|
||||
insertExtraNode: Boolean,
|
||||
disabled: Boolean,
|
||||
},
|
||||
setup(props, { slots, expose }) {
|
||||
const instance = getCurrentInstance();
|
||||
const { csp, prefixCls } = useConfigInject('', props);
|
||||
expose({
|
||||
csp,
|
||||
});
|
||||
let eventIns = null;
|
||||
let clickWaveTimeoutId = null;
|
||||
let animationStartId = null;
|
||||
let animationStart = false;
|
||||
let extraNode = null;
|
||||
let isUnmounted = false;
|
||||
const onTransitionStart = e => {
|
||||
if (isUnmounted) return;
|
||||
|
||||
const node = findDOMNode(instance);
|
||||
if (!e || e.target !== node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!animationStart) {
|
||||
resetEffect(node);
|
||||
}
|
||||
};
|
||||
const onTransitionEnd = (e: any) => {
|
||||
if (!e || e.animationName !== 'fadeEffect') {
|
||||
return;
|
||||
}
|
||||
resetEffect(e.target);
|
||||
};
|
||||
const getAttributeName = () => {
|
||||
const { insertExtraNode } = props;
|
||||
return insertExtraNode
|
||||
? `${prefixCls.value}-click-animating`
|
||||
: `${prefixCls.value}-click-animating-without-extra-node`;
|
||||
};
|
||||
const onClick = (node: HTMLElement, waveColor: string) => {
|
||||
const { insertExtraNode, disabled } = props;
|
||||
if (disabled || !node || isHidden(node) || node.className.indexOf('-leave') >= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
extraNode = document.createElement('div');
|
||||
extraNode.className = `${prefixCls.value}-click-animating-node`;
|
||||
const attributeName = getAttributeName();
|
||||
node.removeAttribute(attributeName);
|
||||
node.setAttribute(attributeName, 'true');
|
||||
// Not white or transparent or grey
|
||||
styleForPesudo = styleForPesudo || document.createElement('style');
|
||||
if (
|
||||
waveColor &&
|
||||
waveColor !== '#ffffff' &&
|
||||
waveColor !== 'rgb(255, 255, 255)' &&
|
||||
isNotGrey(waveColor) &&
|
||||
!/rgba\(\d*, \d*, \d*, 0\)/.test(waveColor) && // any transparent rgba color
|
||||
waveColor !== 'transparent'
|
||||
) {
|
||||
// Add nonce if CSP exist
|
||||
if (csp.value?.nonce) {
|
||||
styleForPesudo.nonce = csp.value.nonce;
|
||||
}
|
||||
extraNode.style.borderColor = waveColor;
|
||||
styleForPesudo.innerHTML = `
|
||||
[${prefixCls.value}-click-animating-without-extra-node='true']::after, .${prefixCls.value}-click-animating-node {
|
||||
--antd-wave-shadow-color: ${waveColor};
|
||||
}`;
|
||||
if (!document.body.contains(styleForPesudo)) {
|
||||
document.body.appendChild(styleForPesudo);
|
||||
}
|
||||
}
|
||||
if (insertExtraNode) {
|
||||
node.appendChild(extraNode);
|
||||
}
|
||||
TransitionEvents.addStartEventListener(node, onTransitionStart);
|
||||
TransitionEvents.addEndEventListener(node, onTransitionEnd);
|
||||
};
|
||||
const resetEffect = (node: HTMLElement) => {
|
||||
if (!node || node === extraNode || !(node instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const { insertExtraNode } = props;
|
||||
const attributeName = getAttributeName();
|
||||
node.setAttribute(attributeName, 'false'); // edge has bug on `removeAttribute` #14466
|
||||
if (styleForPesudo) {
|
||||
styleForPesudo.innerHTML = '';
|
||||
}
|
||||
if (insertExtraNode && extraNode && node.contains(extraNode)) {
|
||||
node.removeChild(extraNode);
|
||||
}
|
||||
TransitionEvents.removeStartEventListener(node, onTransitionStart);
|
||||
TransitionEvents.removeEndEventListener(node, onTransitionEnd);
|
||||
};
|
||||
const bindAnimationEvent = (node: HTMLElement) => {
|
||||
if (
|
||||
!node ||
|
||||
!node.getAttribute ||
|
||||
node.getAttribute('disabled') ||
|
||||
node.className.indexOf('disabled') >= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const newClick = (e: MouseEvent) => {
|
||||
// Fix radio button click twice
|
||||
if ((e.target as any).tagName === 'INPUT' || isHidden(e.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
resetEffect(node);
|
||||
// Get wave color from target
|
||||
const waveColor =
|
||||
getComputedStyle(node).getPropertyValue('border-top-color') || // Firefox Compatible
|
||||
getComputedStyle(node).getPropertyValue('border-color') ||
|
||||
getComputedStyle(node).getPropertyValue('background-color');
|
||||
clickWaveTimeoutId = setTimeout(() => onClick(node, waveColor), 0);
|
||||
raf.cancel(animationStartId);
|
||||
animationStart = true;
|
||||
|
||||
// Render to trigger transition event cost 3 frames. Let's delay 10 frames to reset this.
|
||||
animationStartId = raf(() => {
|
||||
animationStart = false;
|
||||
}, 10);
|
||||
};
|
||||
node.addEventListener('click', newClick, true);
|
||||
return {
|
||||
cancel: () => {
|
||||
node.removeEventListener('click', newClick, true);
|
||||
},
|
||||
};
|
||||
};
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const node = findDOMNode(instance);
|
||||
if (node.nodeType !== 1) {
|
||||
return;
|
||||
}
|
||||
eventIns = bindAnimationEvent(node);
|
||||
});
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
if (eventIns) {
|
||||
eventIns.cancel();
|
||||
}
|
||||
clearTimeout(clickWaveTimeoutId);
|
||||
isUnmounted = true;
|
||||
});
|
||||
return () => {
|
||||
return slots.default?.()[0];
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,164 @@
|
|||
import type { CSSProperties } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, Transition, render, defineComponent, ref } from 'vue';
|
||||
import useState from '../hooks/useState';
|
||||
import { objectType } from '../type';
|
||||
import { getTargetWaveColor } from './util';
|
||||
import wrapperRaf from '../raf';
|
||||
function validateNum(value: number) {
|
||||
return Number.isNaN(value) ? 0 : value;
|
||||
}
|
||||
|
||||
export interface WaveEffectProps {
|
||||
className: string;
|
||||
target: HTMLElement;
|
||||
}
|
||||
|
||||
const WaveEffect = defineComponent({
|
||||
props: {
|
||||
target: objectType<HTMLElement>(),
|
||||
className: String,
|
||||
},
|
||||
setup(props) {
|
||||
const divRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const [color, setWaveColor] = useState<string | null>(null);
|
||||
const [borderRadius, setBorderRadius] = useState<number[]>([]);
|
||||
const [left, setLeft] = useState(0);
|
||||
const [top, setTop] = useState(0);
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
function syncPos() {
|
||||
const { target } = props;
|
||||
const nodeStyle = getComputedStyle(target);
|
||||
|
||||
// Get wave color from target
|
||||
setWaveColor(getTargetWaveColor(target));
|
||||
|
||||
const isStatic = nodeStyle.position === 'static';
|
||||
|
||||
// Rect
|
||||
const { borderLeftWidth, borderTopWidth } = nodeStyle;
|
||||
setLeft(isStatic ? target.offsetLeft : validateNum(-parseFloat(borderLeftWidth)));
|
||||
setTop(isStatic ? target.offsetTop : validateNum(-parseFloat(borderTopWidth)));
|
||||
setWidth(target.offsetWidth);
|
||||
setHeight(target.offsetHeight);
|
||||
|
||||
// Get border radius
|
||||
const {
|
||||
borderTopLeftRadius,
|
||||
borderTopRightRadius,
|
||||
borderBottomLeftRadius,
|
||||
borderBottomRightRadius,
|
||||
} = nodeStyle;
|
||||
|
||||
setBorderRadius(
|
||||
[
|
||||
borderTopLeftRadius,
|
||||
borderTopRightRadius,
|
||||
borderBottomRightRadius,
|
||||
borderBottomLeftRadius,
|
||||
].map(radius => validateNum(parseFloat(radius))),
|
||||
);
|
||||
}
|
||||
// Add resize observer to follow size
|
||||
let resizeObserver: ResizeObserver;
|
||||
let rafId: number;
|
||||
let timeoutId: any;
|
||||
const clear = () => {
|
||||
clearTimeout(timeoutId);
|
||||
wrapperRaf.cancel(rafId);
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
const removeDom = () => {
|
||||
const holder = divRef.value?.parentElement;
|
||||
if (holder) {
|
||||
render(null, holder);
|
||||
if (holder.parentElement) {
|
||||
holder.parentElement.removeChild(holder);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
clear();
|
||||
timeoutId = setTimeout(() => {
|
||||
removeDom();
|
||||
}, 5000);
|
||||
const { target } = props;
|
||||
if (target) {
|
||||
// We need delay to check position here
|
||||
// since UI may change after click
|
||||
rafId = wrapperRaf(() => {
|
||||
syncPos();
|
||||
|
||||
setEnabled(true);
|
||||
});
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
resizeObserver = new ResizeObserver(syncPos);
|
||||
|
||||
resizeObserver.observe(target);
|
||||
}
|
||||
}
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
clear();
|
||||
});
|
||||
|
||||
const onTransitionend = (e: TransitionEvent) => {
|
||||
if (e.propertyName === 'opacity') {
|
||||
removeDom();
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
if (!enabled.value) {
|
||||
return null;
|
||||
}
|
||||
const waveStyle = {
|
||||
left: `${left.value}px`,
|
||||
top: `${top.value}px`,
|
||||
width: `${width.value}px`,
|
||||
height: `${height.value}px`,
|
||||
borderRadius: borderRadius.value.map(radius => `${radius}px`).join(' '),
|
||||
} as CSSProperties & {
|
||||
[name: string]: number | string;
|
||||
};
|
||||
|
||||
if (color) {
|
||||
waveStyle['--wave-color'] = color.value as string;
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
appear
|
||||
name="wave-motion"
|
||||
appearFromClass="wave-motion-appear"
|
||||
appearActiveClass="wave-motion-appear"
|
||||
appearToClass="wave-motion-appear wave-motion-appear-active"
|
||||
>
|
||||
<div
|
||||
ref={divRef}
|
||||
class={props.className}
|
||||
style={waveStyle}
|
||||
onTransitionend={onTransitionend}
|
||||
/>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function showWaveEffect(node: HTMLElement, className: string) {
|
||||
// Create holder
|
||||
const holder = document.createElement('div');
|
||||
holder.style.position = 'absolute';
|
||||
holder.style.left = `0px`;
|
||||
holder.style.top = `0px`;
|
||||
node?.insertBefore(holder, node?.firstChild);
|
||||
|
||||
render(<WaveEffect target={node} className={className} />, holder);
|
||||
}
|
||||
|
||||
export default showWaveEffect;
|
|
@ -0,0 +1,96 @@
|
|||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import useConfigInject from '../../config-provider/hooks/useConfigInject';
|
||||
import isVisible from '../../vc-util/Dom/isVisible';
|
||||
import classNames from '../classNames';
|
||||
import { findDOMNode } from '../props-util';
|
||||
import useStyle from './style';
|
||||
import useWave from './useWave';
|
||||
|
||||
export interface WaveProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'Wave',
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const instance = getCurrentInstance();
|
||||
const { prefixCls } = useConfigInject('wave', props);
|
||||
|
||||
// ============================== Style ===============================
|
||||
const [, hashId] = useStyle(prefixCls);
|
||||
|
||||
// =============================== Wave ===============================
|
||||
const showWave = useWave(
|
||||
instance,
|
||||
computed(() => classNames(prefixCls.value, hashId.value)),
|
||||
);
|
||||
let onClick: (e: MouseEvent) => void;
|
||||
const clear = () => {
|
||||
const node = findDOMNode(instance);
|
||||
node.removeEventListener('click', onClick, true);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
watch(
|
||||
() => props.disabled,
|
||||
() => {
|
||||
clear();
|
||||
nextTick(() => {
|
||||
const node = findDOMNode(instance);
|
||||
|
||||
if (!node || node.nodeType !== 1 || props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Click handler
|
||||
const onClick = (e: MouseEvent) => {
|
||||
// Fix radio button click twice
|
||||
if (
|
||||
(e.target as HTMLElement).tagName === 'INPUT' ||
|
||||
!isVisible(e.target as HTMLElement) ||
|
||||
// No need wave
|
||||
!node.getAttribute ||
|
||||
node.getAttribute('disabled') ||
|
||||
(node as HTMLInputElement).disabled ||
|
||||
node.className.includes('disabled') ||
|
||||
node.className.includes('-leave')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
showWave();
|
||||
};
|
||||
|
||||
// Bind events
|
||||
node.addEventListener('click', onClick, true);
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
flush: 'post',
|
||||
},
|
||||
);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
clear();
|
||||
});
|
||||
|
||||
return () => {
|
||||
// ============================== Render ==============================
|
||||
const children = slots.default?.()[0];
|
||||
return children;
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { genComponentStyleHook } from '../../theme/internal';
|
||||
import type { FullToken, GenerateStyle } from '../../theme/internal';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ComponentToken {}
|
||||
|
||||
export type WaveToken = FullToken<'Wave'>;
|
||||
|
||||
const genWaveStyle: GenerateStyle<WaveToken> = token => {
|
||||
const { componentCls, colorPrimary } = token;
|
||||
return {
|
||||
[componentCls]: {
|
||||
position: 'absolute',
|
||||
background: 'transparent',
|
||||
pointerEvents: 'none',
|
||||
boxSizing: 'border-box',
|
||||
color: `var(--wave-color, ${colorPrimary})`,
|
||||
|
||||
boxShadow: `0 0 0 0 currentcolor`,
|
||||
opacity: 0.2,
|
||||
|
||||
// =================== Motion ===================
|
||||
'&.wave-motion-appear': {
|
||||
transition: [
|
||||
`box-shadow 0.4s ${token.motionEaseOutCirc}`,
|
||||
`opacity 2s ${token.motionEaseOutCirc}`,
|
||||
].join(','),
|
||||
|
||||
'&-active': {
|
||||
boxShadow: `0 0 0 6px currentcolor`,
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default genComponentStyleHook('Wave', token => [genWaveStyle(token)]);
|
|
@ -0,0 +1,16 @@
|
|||
import type { ComponentInternalInstance, Ref } from 'vue';
|
||||
import { findDOMNode } from '../props-util';
|
||||
import showWaveEffect from './WaveEffect';
|
||||
|
||||
export default function useWave(
|
||||
instance: ComponentInternalInstance | null,
|
||||
className: Ref<string>,
|
||||
): VoidFunction {
|
||||
function showWave() {
|
||||
const node = findDOMNode(instance);
|
||||
|
||||
showWaveEffect(node, className.value);
|
||||
}
|
||||
|
||||
return showWave;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
export function isNotGrey(color: string) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const match = (color || '').match(/rgba?\((\d*), (\d*), (\d*)(, [\d.]*)?\)/);
|
||||
if (match && match[1] && match[2] && match[3]) {
|
||||
return !(match[1] === match[2] && match[2] === match[3]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isValidWaveColor(color: string) {
|
||||
return (
|
||||
color &&
|
||||
color !== '#fff' &&
|
||||
color !== '#ffffff' &&
|
||||
color !== 'rgb(255, 255, 255)' &&
|
||||
color !== 'rgba(255, 255, 255, 1)' &&
|
||||
isNotGrey(color) &&
|
||||
!/rgba\((?:\d*, ){3}0\)/.test(color) && // any transparent rgba color
|
||||
color !== 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
export function getTargetWaveColor(node: HTMLElement) {
|
||||
const { borderTopColor, borderColor, backgroundColor } = getComputedStyle(node);
|
||||
if (isValidWaveColor(borderTopColor)) {
|
||||
return borderTopColor;
|
||||
}
|
||||
if (isValidWaveColor(borderColor)) {
|
||||
return borderColor;
|
||||
}
|
||||
if (isValidWaveColor(backgroundColor)) {
|
||||
return backgroundColor;
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -154,7 +154,7 @@ const Switch = defineComponent({
|
|||
|
||||
return () =>
|
||||
wrapSSR(
|
||||
<Wave insertExtraNode>
|
||||
<Wave>
|
||||
<button
|
||||
{...omit(props, [
|
||||
'prefixCls',
|
||||
|
|
|
@ -48,7 +48,7 @@ import type { ComponentToken as UploadComponentToken } from '../../upload/style'
|
|||
// import type { ComponentToken as TourComponentToken } from '../../tour/style';
|
||||
import type { ComponentToken as QRCodeComponentToken } from '../../qrcode/style';
|
||||
// import type { ComponentToken as AppComponentToken } from '../../app/style';
|
||||
// import type { ComponentToken as WaveToken } from '../../_util/wave/style';
|
||||
import type { ComponentToken as WaveToken } from '../../_util/wave/style';
|
||||
|
||||
export interface ComponentTokenMap {
|
||||
Affix?: {};
|
||||
|
@ -117,5 +117,5 @@ export interface ComponentTokenMap {
|
|||
// App?: AppComponentToken;
|
||||
|
||||
// /** @private Internal TS definition. Do not use. */
|
||||
// Wave?: WaveToken;
|
||||
Wave?: WaveToken;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue