foundation.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  2. import touchEventPolyfill from '../utils/touchPolyfill';
  3. import warning from '../utils/warning';
  4. import { handlePrevent } from '../utils/a11y';
  5. export interface Marks{
  6. [key: number]: string
  7. }
  8. export type tipFormatterBasicType = string | number | boolean | null;
  9. export interface SliderProps{
  10. defaultValue?: number | number[];
  11. disabled?: boolean;
  12. showMarkLabel?: boolean;
  13. included?: boolean; // Whether to juxtapose. Allow dragging
  14. marks?: Marks; // Scale
  15. max?: number;
  16. min?: number;
  17. range?: boolean; // Whether both sides
  18. step?: number;
  19. tipFormatter?: (value: tipFormatterBasicType | tipFormatterBasicType[]) => any;
  20. value?: number | number[];
  21. vertical?: boolean;
  22. onAfterChange?: (value: SliderProps['value']) => void; // triggered when mouse up and clicked
  23. onChange?: (value: SliderProps['value']) => void;
  24. onMouseUp?: (e: any) => void;
  25. tooltipOnMark?: boolean;
  26. tooltipVisible?: boolean;
  27. style?: Record<string, any>;
  28. showArrow?: boolean;
  29. className?: string;
  30. showBoundary?: boolean;
  31. railStyle?: Record<string, any>;
  32. verticalReverse?: boolean;
  33. 'aria-label'?: string;
  34. 'aria-labelledby'?: string;
  35. 'aria-valuetext'?: string;
  36. getAriaValueText?: (value: number, index?: number) => string;
  37. handleDot?: {
  38. size?: string;
  39. color?: string
  40. } & ({
  41. size?: string;
  42. color?: string
  43. }[])
  44. }
  45. export interface SliderState {
  46. currentValue: number | number[];
  47. min: number;
  48. max: number;
  49. focusPos: 'min' | 'max' | '';
  50. onChange: (value: SliderProps['value']) => void;
  51. disabled: SliderProps['disabled'];
  52. chooseMovePos: 'min' | 'max' | '';
  53. isDrag: boolean;
  54. clickValue: 0;
  55. showBoundary: boolean;
  56. isInRenderTree: boolean;
  57. firstDotFocusVisible: boolean;
  58. secondDotFocusVisible: boolean
  59. }
  60. export interface SliderLengths{
  61. sliderX: number;
  62. sliderY: number;
  63. sliderWidth: number;
  64. sliderHeight: number
  65. }
  66. export interface ScrollParentVal{
  67. scrollTop: number;
  68. scrollLeft: number
  69. }
  70. export interface OverallVars{
  71. dragging: boolean[]
  72. }
  73. export interface SliderAdapter extends DefaultAdapter<SliderProps, SliderState>{
  74. getSliderLengths: () => SliderLengths;
  75. getParentRect: () => DOMRect | void;
  76. getScrollParentVal: () => ScrollParentVal;
  77. isEventFromHandle: (e: any) => boolean;
  78. getOverallVars: () => OverallVars;
  79. updateDisabled: (disabled: SliderState['disabled']) => void;
  80. transNewPropsToState: <K extends keyof SliderState>(stateObj: Pick<SliderState, K>, callback?: () => void) => void;
  81. notifyChange: (callbackValue: number | number[]) => void;
  82. setDragging: (value: boolean[]) => void;
  83. updateCurrentValue: (value: SliderState['currentValue']) => void;
  84. setOverallVars: (key: string, value: any) => void;
  85. getMinHandleEl: () => HTMLSpanElement;
  86. getMaxHandleEl: () => HTMLSpanElement;
  87. onHandleDown: (e: any) => any;
  88. onHandleMove: (mousePos: number, isMin: boolean, stateChangeCallback?: () => void, clickTrack?: boolean, outPutValue?: number | number[]) => boolean | void;
  89. setEventDefault: (e: any) => void;
  90. setStateVal: (state: keyof SliderState, value: any) => void;
  91. onHandleEnter: (position: SliderState['focusPos']) => void;
  92. onHandleLeave: () => void;
  93. onHandleUpBefore: (e: any) => void;
  94. onHandleUpAfter: () => void;
  95. unSubscribeEventListener: () => void;
  96. checkAndUpdateIsInRenderTreeState: () => boolean
  97. }
  98. export default class SliderFoundation extends BaseFoundation<SliderAdapter> {
  99. private _dragOffset: number;
  100. constructor(adapter: SliderAdapter) {
  101. super({ ...SliderFoundation.defaultAdapter, ...adapter });
  102. }
  103. init() {
  104. this._checkCurrentValue();
  105. this._dragOffset = 0;
  106. }
  107. _checkCurrentValue() {
  108. const { currentValue, min, max } = this.getStates();
  109. let checked;
  110. if (Array.isArray(currentValue)) {
  111. checked = [];
  112. checked[0] = this._checkValidity(currentValue[0], min, max);
  113. checked[1] = this._checkValidity(currentValue[1], min, max);
  114. } else {
  115. checked = this._checkValidity(currentValue, min, max);
  116. }
  117. this._adapter.updateCurrentValue(checked);
  118. }
  119. /**
  120. * Untie event
  121. * @memberof SliderFoundation
  122. */
  123. destroy() {
  124. // debugger
  125. this._adapter.unSubscribeEventListener();
  126. }
  127. /**
  128. * Calculate the percentage corresponding to the current value for style calculation
  129. * @{}
  130. *
  131. * @memberof SliderFoundation
  132. */
  133. getMinAndMaxPercent = (value: number | number[]) => {
  134. // debugger
  135. const { range, min, max } = this._adapter.getProps();
  136. const minPercent = range ? (value[0] - min) / (max - min) : (value as number - min) / (max - min);
  137. const maxPercent = range ? (value[1] - min) / (max - min) : 1;
  138. return { min: this._checkValidity(minPercent), max: this._checkValidity(maxPercent) };
  139. };
  140. /**
  141. * Check if value is out of range
  142. * @memberof SliderFoundation
  143. */
  144. _checkValidity = (value: number, min = 0, max = 1) => {
  145. const checked = value > max ?
  146. max :
  147. value < min ?
  148. min :
  149. value;
  150. return checked;
  151. };
  152. /**
  153. * When render handle, the display and content of the tooltip are calculated according to the conditions
  154. * @visible: props passed in by the component
  155. * @formatter: tooltip content formatting function
  156. * @memberof SliderFoundation
  157. */
  158. computeHandleVisibleVal = (visible: SliderProps['tooltipVisible'], formatter: SliderProps['tipFormatter'], range: SliderProps['range']) => {
  159. // debugger;
  160. const { focusPos, currentValue } = this._adapter.getStates();
  161. const tipVisible = { min: false, max: false };
  162. let tipChildren;
  163. if (formatter) {
  164. tipChildren = {
  165. min: range ?
  166. formatter(this.outPutValue(currentValue[0])) :
  167. formatter(this.outPutValue(currentValue)),
  168. max: range ? formatter(this.outPutValue(currentValue[1])) : null,
  169. };
  170. } else {
  171. tipChildren = {
  172. min: range ? this.outPutValue(currentValue[0]) : this.outPutValue(currentValue),
  173. max: range ? this.outPutValue(currentValue[1]) : null,
  174. };
  175. }
  176. if (visible) {
  177. tipVisible.min = true;
  178. tipVisible.max = true;
  179. } else if (typeof visible === 'undefined' && formatter) {
  180. if (focusPos === 'min') {
  181. tipVisible.min = true;
  182. } else if (focusPos === 'max') {
  183. tipVisible.max = true;
  184. }
  185. }
  186. const result = {
  187. tipVisible,
  188. tipChildren,
  189. };
  190. return result;
  191. };
  192. /**
  193. * Calculate whether the value passed in is valid
  194. *
  195. * @memberof SliderFoundation
  196. */
  197. valueFormatIsCorrect = (value: SliderProps['value']) => {
  198. if (Array.isArray(value)) {
  199. return typeof value[0] === 'number' && typeof value[0] === 'number';
  200. } else {
  201. return typeof value === 'number';
  202. }
  203. };
  204. /**
  205. * Fix the mouse position to position the parent container relative to the position
  206. *
  207. * @memberof SliderFoundation
  208. */
  209. handleMousePos = (clientX: number, clientY: number) => {
  210. const parentRect = this._adapter.getParentRect();
  211. const scrollParent = this._adapter.getScrollParentVal();
  212. const parentX = parentRect ? parentRect.left : 0;
  213. const parentY = parentRect ? parentRect.top : 0;
  214. return { x: clientX - parentX + scrollParent.scrollLeft, y: clientY - parentY + scrollParent.scrollTop };
  215. };
  216. /**
  217. * Provides the nearest scrollable parent node of the current node, which is used to calculate the scrollTop and scrollLeft attributes
  218. *
  219. * @memberof SliderFoundation
  220. */
  221. getScrollParent = (element: HTMLElement) => {
  222. // TODO: move window document out of foundation.
  223. const el = element;
  224. const regex = /(auto|scroll)/;
  225. const style = (node: Element, prop: string) => window.getComputedStyle(node, null).getPropertyValue(prop);
  226. const scroll = (node: Element) => regex.test(style(node, 'overflow') + style(node, 'overflow-y') + style(node, 'overflow-x'));
  227. const scrollParent = (node: Element): Element => (
  228. !node || (node === document.body || !(node instanceof Element)) ? document.body : scroll(node) ? node : scrollParent(node.parentNode as Element)
  229. );
  230. return scrollParent(el);
  231. };
  232. /**
  233. * Fixed the event location, beyond the maximum, minimum, left and right, etc. directly modified to the effective location
  234. *
  235. * @memberof SliderFoundation
  236. */
  237. checkMeetMinMax = (position: number) => {
  238. // Returns the length of the distance to the left
  239. const { vertical, verticalReverse, range } = this._adapter.getProps();
  240. const value = this._adapter.getState('currentValue');
  241. const currentPos = this.transValueToPos(value);
  242. const { sliderX, sliderY, sliderWidth, sliderHeight } = this._adapter.getSliderLengths();
  243. const { chooseMovePos, isDrag } = this._adapter.getStates();
  244. const len = vertical ? sliderHeight : sliderWidth;
  245. let startPos;
  246. if (vertical && verticalReverse) {
  247. startPos = sliderY + len;
  248. } else {
  249. startPos = vertical ? sliderY : sliderX;
  250. }
  251. // startPos = chooseMovePos === 'max' && isDrag ? currentPos[0] : startPos;
  252. let endPos;
  253. if (vertical && verticalReverse) {
  254. endPos = sliderY;
  255. } else {
  256. endPos = vertical ? sliderY + sliderHeight : sliderX + sliderWidth;
  257. }
  258. // endPos = chooseMovePos === 'min' && isDrag && range ? currentPos[1] : endPos;
  259. if (vertical && verticalReverse) {
  260. if (position >= startPos) {
  261. position = startPos;
  262. } else if (position <= endPos) {
  263. position = endPos;
  264. }
  265. } else {
  266. if (position <= startPos) {
  267. position = startPos;
  268. } else if (position >= endPos) {
  269. position = endPos;
  270. }
  271. }
  272. return position;
  273. };
  274. /**
  275. * Converting location information to value requires processing if step is not 1 (invalid move returns false)
  276. *
  277. * @memberof SliderFoundation
  278. */
  279. transPosToValue = (mousePos: number, isMin: boolean) => {
  280. const pos = this.checkMeetMinMax(mousePos);
  281. const { min, max, currentValue } = this._adapter.getStates();
  282. const { range, vertical, step, verticalReverse } = this._adapter.getProps();
  283. const { sliderX, sliderY, sliderWidth, sliderHeight } = this._adapter.getSliderLengths();
  284. const startPos = vertical ? sliderY : sliderX;
  285. const len = vertical ? sliderHeight : sliderWidth;
  286. let stepValue;
  287. if (vertical && verticalReverse) {
  288. //isMin = !isMin;
  289. stepValue = ((startPos + len - pos) / len) * (max - min) + min;
  290. } else {
  291. stepValue = ((pos - startPos) / len) * (max - min) + min;
  292. }
  293. let compareValue;
  294. if (range) {
  295. compareValue = isMin ? currentValue[0] : currentValue[1];
  296. } else {
  297. compareValue = currentValue;
  298. }
  299. if (step !== 1) { // Find nearest step point
  300. stepValue = Math.round(stepValue / step) * step;
  301. }
  302. if (range && stepValue !== compareValue) {
  303. return isMin ? [stepValue, currentValue[1]] : [currentValue[0], stepValue];
  304. } else if (!range && stepValue !== compareValue) {
  305. return (stepValue);
  306. } else {
  307. return false;
  308. }
  309. };
  310. /**
  311. * Convert value values into location information
  312. *
  313. * @memberof SliderFoundation
  314. */
  315. transValueToPos = (value: SliderProps['value']) => {
  316. const { min, max } = this._adapter.getStates();
  317. const { vertical, range, verticalReverse } = this._adapter.getProps();
  318. const { sliderX, sliderY, sliderWidth, sliderHeight } = this._adapter.getSliderLengths();
  319. const startPos = vertical ? sliderY : sliderX;
  320. const len = vertical ? sliderHeight : sliderWidth;
  321. if (range) {
  322. return [((value[0] - min) * len) / (max - min) + startPos, ((value[1] - min) * len) / (max - min) + startPos];
  323. } else {
  324. return ((value as number - min) * len) / (max - min) + startPos;
  325. }
  326. };
  327. /**
  328. * Determine whether the mark should be highlighted: valid interval and include = false
  329. *
  330. * @memberof SliderFoundation
  331. */
  332. isMarkActive = (mark: number) => {
  333. const { min, max, range, included } = this._adapter.getProps();
  334. const currentValue = this._adapter.getState('currentValue');
  335. if (typeof (mark / 1) === 'number' && mark >= min && mark <= max) {
  336. if (range) {
  337. return (mark > currentValue[1] || mark < currentValue[0]) && included ? 'unActive' : 'active';
  338. } else {
  339. return mark <= currentValue && included ? 'active' : 'unActive';
  340. }
  341. } else {
  342. return false;
  343. }
  344. };
  345. /**
  346. * onchange output conversion, default rounding without decimal, step less than 1 has decimal
  347. *
  348. * @memberof SliderFoundation
  349. */
  350. outPutValue = (inputValue: SliderProps['value']) => {
  351. const checkHowManyDecimals = (num: number)=>{
  352. const reg = /^\d+(\.\d+)?$/;
  353. if (reg.test(String(num))) {
  354. return num.toString().split('.')[1]?.length ?? 0;
  355. }
  356. return 0;
  357. };
  358. const step = this._adapter.getProp('step');
  359. const transWay = (()=>{
  360. const decimals = checkHowManyDecimals(step);
  361. const multipler = Math.pow(10, decimals);
  362. return (value: number)=>{
  363. return Math.round(value * multipler) / multipler;
  364. };
  365. })();
  366. if (Array.isArray(inputValue)) {
  367. return [transWay(inputValue[0]), transWay(inputValue[1])];
  368. } else {
  369. return transWay(inputValue);
  370. }
  371. };
  372. handleDisabledChange = (disabled: SliderState['disabled']) => {
  373. this._adapter.updateDisabled(disabled);
  374. };
  375. checkAndUpdateIsInRenderTreeState = () => this._adapter.checkAndUpdateIsInRenderTreeState();
  376. calculateOutputValue = (position: number, isMin: boolean): undefined | number | number[] => {
  377. const moveValue = this.transPosToValue(position, isMin);
  378. if (moveValue === false) {
  379. return undefined;
  380. }
  381. return this.outPutValue(moveValue);
  382. }
  383. /**
  384. *
  385. *
  386. * @memberof SliderFoundation
  387. */
  388. handleValueChange = (prevValue: SliderProps['value'], nextValue: SliderProps['value']) => {
  389. const { min, max } = this._adapter.getStates();
  390. let resultState = null;
  391. const disableState = {};
  392. if (this.valueFormatIsCorrect(nextValue)) {
  393. if (Array.isArray(prevValue) && Array.isArray(nextValue)) {
  394. nextValue = [
  395. nextValue[0] < min ? min : nextValue[0], // Math.round(nextValue[0]),
  396. nextValue[1] > max ? max : nextValue[1], // Math.round(nextValue[1])
  397. ];
  398. // this._adapter.notifyChange(this.outPutValue(nextValue));
  399. resultState = Object.assign(disableState, {
  400. currentValue: nextValue,
  401. });
  402. }
  403. if (typeof prevValue === 'number' && typeof nextValue === 'number') {
  404. if (nextValue > max) {
  405. nextValue = max;
  406. } else {
  407. nextValue = nextValue < min ? min : nextValue; // Math.round(nextValue);
  408. }
  409. // this._adapter.notifyChange(this.outPutValue(nextValue));
  410. resultState = Object.assign(disableState, {
  411. currentValue: nextValue,
  412. });
  413. }
  414. } else {
  415. resultState = disableState;
  416. }
  417. if (resultState) {
  418. this._adapter.transNewPropsToState(resultState);
  419. }
  420. };
  421. onHandleDown = (e: any, handler: any) => {
  422. this._adapter.onHandleDown(e);
  423. const disabled = this._adapter.getState('disabled');
  424. const { vertical } = this._adapter.getProps();
  425. const { dragging } = this._adapter.getOverallVars();
  426. if (disabled) {
  427. return false;
  428. }
  429. this._adapter.setStateVal('isDrag', true);
  430. this._adapter.setStateVal('chooseMovePos', handler);
  431. if (handler === 'min') {
  432. this._adapter.setDragging([true, dragging[1]]);
  433. } else {
  434. this._adapter.setDragging([dragging[0], true]);
  435. }
  436. const mousePos = this.handleMousePos(e.clientX, e.clientY);
  437. let pos = vertical ? mousePos.y : mousePos.x;
  438. if (!this._adapter.isEventFromHandle(e)) {
  439. this._dragOffset = 0;
  440. } else {
  441. const handlePosition = this._getHandleCenterPosition(vertical, e.target);
  442. this._dragOffset = vertical ? pos - handlePosition : pos - handlePosition;
  443. pos = handlePosition;
  444. }
  445. return true;
  446. };
  447. onHandleMove = (e: any) => {
  448. this._adapter.setEventDefault(e);
  449. const { disabled, chooseMovePos } = this._adapter.getStates();
  450. const { vertical } = this._adapter.getProps();
  451. const { dragging } = this._adapter.getOverallVars();
  452. if (disabled) {
  453. return false;
  454. }
  455. this.onHandleEnter(chooseMovePos);
  456. const mousePos = this.handleMousePos(e.clientX, e.clientY);
  457. let pagePos = vertical ? mousePos.y : mousePos.x;
  458. pagePos = pagePos - this._dragOffset;
  459. if ((chooseMovePos === 'min' && dragging[0]) || (chooseMovePos === 'max' && dragging[1])) {
  460. const outPutValue = this.calculateOutputValue(pagePos, chooseMovePos === 'min' );
  461. if (outPutValue === undefined) {
  462. return false;
  463. }
  464. this._adapter.notifyChange(outPutValue);
  465. // allow drag for controlled component, so no _isControlledComponent check
  466. this._adapter.onHandleMove(pagePos, chooseMovePos === 'min', undefined, false, outPutValue);
  467. }
  468. return true;
  469. };
  470. // run when user touch left or right handle.
  471. onHandleTouchStart = (e: any, handler: 'min' | 'max') => {
  472. const handleMinDom = this._adapter.getMinHandleEl();
  473. const handleMaxDom = this._adapter.getMaxHandleEl();
  474. if (e.target === handleMinDom || e.target === handleMaxDom) {
  475. handlePrevent(e);
  476. const touch = touchEventPolyfill(e.touches[0], e);
  477. this.onHandleDown(touch, handler);
  478. }
  479. };
  480. onHandleTouchMove = (e: any) => {
  481. const handleMinDom = this._adapter.getMinHandleEl();
  482. const handleMaxDom = this._adapter.getMaxHandleEl();
  483. if (e.target === handleMinDom || e.target === handleMaxDom) {
  484. const touch = touchEventPolyfill(e.touches[0], e);
  485. this.onHandleMove(touch);
  486. }
  487. };
  488. onHandleEnter = (pos: SliderState['focusPos']) => {
  489. // debugger;
  490. // this._adapter.setEventDefault(e);
  491. const { disabled, focusPos } = this._adapter.getStates();
  492. if (!disabled) {
  493. if (!focusPos && pos !== focusPos) {
  494. this._adapter.onHandleEnter(pos);
  495. }
  496. }
  497. };
  498. onHandleLeave = () => {
  499. // this._adapter.setEventDefault(e);
  500. const disabled = this._adapter.getState('disabled');
  501. if (!disabled && this.getStates()['focusPos'] === "") {
  502. this._adapter.onHandleLeave();
  503. }
  504. };
  505. onHandleUp = (e: any) => {
  506. this._adapter.onHandleUpBefore(e);
  507. // const value = this._adapter.getProp('value');
  508. const { disabled, chooseMovePos } = this._adapter.getStates();
  509. const { dragging } = this._adapter.getOverallVars();
  510. if (disabled) {
  511. return false;
  512. }
  513. if (chooseMovePos === 'min') {
  514. this._adapter.setDragging([false, dragging[1]]);
  515. } else {
  516. this._adapter.setDragging([dragging[0], false]);
  517. }
  518. this._adapter.setStateVal('isDrag', false);
  519. this._adapter.onHandleLeave();
  520. this._adapter.onHandleUpAfter();
  521. return true;
  522. };
  523. _handleValueDecreaseWithKeyBoard = (step: number, handler: 'min'| 'max') => {
  524. const { min, currentValue } = this.getStates();
  525. const { range } = this.getProps();
  526. if (handler === 'min') {
  527. if (range) {
  528. let newMinValue = currentValue[0] - step;
  529. newMinValue = newMinValue < min ? min : newMinValue;
  530. return [newMinValue, currentValue[1]];
  531. } else {
  532. let newMinValue = currentValue - step;
  533. newMinValue = newMinValue < min ? min : newMinValue;
  534. return newMinValue;
  535. }
  536. } else {
  537. let newMaxValue = currentValue[1] - step;
  538. newMaxValue = newMaxValue < currentValue[0] ? currentValue[0] : newMaxValue;
  539. return [currentValue[0], newMaxValue];
  540. }
  541. }
  542. _handleValueIncreaseWithKeyBoard = (step: number, handler: 'min'| 'max') => {
  543. const { max, currentValue } = this.getStates();
  544. const { range } = this.getProps();
  545. if (handler === 'min') {
  546. if (range) {
  547. let newMinValue = currentValue[0] + step;
  548. newMinValue = newMinValue > currentValue[1] ? currentValue[1] : newMinValue;
  549. return [newMinValue, currentValue[1]];
  550. } else {
  551. let newMinValue = currentValue + step;
  552. newMinValue = newMinValue > max ? max : newMinValue;
  553. return newMinValue;
  554. }
  555. } else {
  556. let newMaxValue = currentValue[1] + step;
  557. newMaxValue = newMaxValue > max ? max : newMaxValue;
  558. return [currentValue[0], newMaxValue];
  559. }
  560. }
  561. _handleHomeKey = (handler: 'min'| 'max') => {
  562. const { min, currentValue } = this.getStates();
  563. const { range } = this.getProps();
  564. if (handler === 'min') {
  565. if (range) {
  566. return [min, currentValue[1]];
  567. } else {
  568. return min;
  569. }
  570. } else {
  571. return [currentValue[0], currentValue[0]];
  572. }
  573. }
  574. _handleEndKey = (handler: 'min'| 'max') => {
  575. const { max, currentValue } = this.getStates();
  576. const { range } = this.getProps();
  577. if (handler === 'min') {
  578. if (range) {
  579. return [currentValue[1], currentValue[1]];
  580. } else {
  581. return max;
  582. }
  583. } else {
  584. return [currentValue[0], max];
  585. }
  586. }
  587. handleKeyDown = (event: any, handler: 'min'| 'max') => {
  588. const { min, max, currentValue } = this.getStates();
  589. const { step, range } = this.getProps();
  590. let outputValue;
  591. switch (event.key) {
  592. case "ArrowLeft":
  593. case "ArrowDown":
  594. outputValue = this._handleValueDecreaseWithKeyBoard(step, handler);
  595. break;
  596. case "ArrowRight":
  597. case "ArrowUp":
  598. outputValue = this._handleValueIncreaseWithKeyBoard(step, handler);
  599. break;
  600. case "PageUp":
  601. outputValue = this._handleValueIncreaseWithKeyBoard(10 * step, handler);
  602. break;
  603. case "PageDown":
  604. outputValue = this._handleValueDecreaseWithKeyBoard(10 * step, handler);
  605. break;
  606. case "Home":
  607. outputValue = this._handleHomeKey(handler);
  608. break;
  609. case "End":
  610. outputValue = this._handleEndKey(handler);
  611. break;
  612. case 'default':
  613. break;
  614. }
  615. if (["ArrowLeft", "ArrowDown", "ArrowRight", "ArrowUp", "PageUp", "PageDown", "Home", "End"].includes(event.key)) {
  616. let update = true;
  617. if (Array.isArray(currentValue)) {
  618. update = !(currentValue[0] === outputValue[0] && currentValue[1] === outputValue[1]);
  619. } else {
  620. update = currentValue !== outputValue;
  621. }
  622. if (update) {
  623. this._adapter.updateCurrentValue(outputValue);
  624. this._adapter.notifyChange(outputValue);
  625. }
  626. handlePrevent(event);
  627. }
  628. }
  629. onFocus = (e: any, handler: 'min'| 'max') => {
  630. handlePrevent(e);
  631. const { target } = e;
  632. try {
  633. if (target.matches(':focus-visible')) {
  634. if (handler === 'min') {
  635. this._adapter.setStateVal('firstDotFocusVisible', true);
  636. } else {
  637. this._adapter.setStateVal('secondDotFocusVisible', true);
  638. }
  639. }
  640. } catch (error) {
  641. warning(true, 'Warning: [Semi Slider] The current browser does not support the focus-visible');
  642. }
  643. }
  644. onBlur = (e: any, handler: 'min'| 'max') => {
  645. const { firstDotFocusVisible, secondDotFocusVisible } = this.getStates();
  646. if (handler === 'min') {
  647. firstDotFocusVisible && this._adapter.setStateVal('firstDotFocusVisible', false);
  648. } else {
  649. secondDotFocusVisible && this._adapter.setStateVal('secondDotFocusVisible', false);
  650. }
  651. }
  652. handleWrapClick = (e: any) => {
  653. const { disabled, isDrag } = this._adapter.getStates();
  654. if (isDrag || disabled || this._adapter.isEventFromHandle(e)) {
  655. return;
  656. }
  657. const { vertical } = this.getProps();
  658. const mousePos = this.handleMousePos(e.clientX, e.clientY);
  659. const position = vertical ? mousePos.y : mousePos.x;
  660. const isMin = this.checkWhichHandle(position);
  661. const outPutValue = this.calculateOutputValue(position, isMin);
  662. if (outPutValue === undefined) {
  663. return;
  664. }
  665. this._adapter.notifyChange(outPutValue);
  666. // check if is controlled component
  667. if (this._isControlledComponent()) {
  668. // only perform callback ops, skip UI update
  669. return;
  670. }
  671. // trigger UI state update
  672. this.setHandlePos(position, isMin, true, outPutValue);
  673. };
  674. /**
  675. * Move the slider to the current click position
  676. *
  677. * @memberof SliderFoundation
  678. */
  679. setHandlePos = (position: number, isMin: boolean, clickTrack = false, outPutValue: number | number[]) => {
  680. this._adapter.onHandleMove(position, isMin, () => this._adapter.onHandleUpAfter(), clickTrack, outPutValue);
  681. };
  682. /**
  683. * Determine which slider should be moved currently
  684. *
  685. * @memberof SliderFoundation
  686. */
  687. checkWhichHandle = (pagePos: number) => {
  688. const { vertical, verticalReverse } = this.getProps();
  689. const { currentValue } = this._adapter.getStates();
  690. const currentPos = this.transValueToPos(currentValue);
  691. let isMin = true;
  692. if (Array.isArray(currentPos)) {
  693. // Slide on both sides
  694. if (
  695. pagePos > currentPos[1] ||
  696. Math.abs(pagePos - currentPos[0]) > Math.abs(pagePos - currentPos[1])
  697. ) {
  698. isMin = false;
  699. }
  700. }
  701. if (vertical && verticalReverse) {
  702. isMin = !isMin;
  703. }
  704. return isMin;
  705. };
  706. handleWrapperEnter = () => {
  707. this._adapter.setStateVal('showBoundary', true);
  708. };
  709. handleWrapperLeave = () => {
  710. this._adapter.setStateVal('showBoundary', false);
  711. };
  712. private _getHandleCenterPosition(vertical: boolean, handle: HTMLElement) {
  713. const pos = handle.getBoundingClientRect();
  714. const { x, y } = this.handleMousePos(pos.left + (pos.width * 0.5), pos.top + (pos.height * 0.5));
  715. return vertical ? y : x;
  716. }
  717. }