foundation.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. /* eslint-disable no-param-reassign */
  2. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  3. import warning from '../utils/warning';
  4. export interface RatingAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  5. focus: () => void;
  6. getStarDOM: (index: number) => Element;
  7. notifyHoverChange: (hoverValue: number, clearedValue: number) => void;
  8. updateValue: (value: number) => void;
  9. clearValue: (clearedValue: number) => void;
  10. notifyFocus: (e: any) => void;
  11. notifyBlur: (e: any) => void;
  12. notifyKeyDown: (e: any) => void;
  13. setEmptyStarFocusVisible: (focusVisible: boolean) => void
  14. }
  15. export default class RatingFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<RatingAdapter<P, S>, P, S> {
  16. constructor(adapter: RatingAdapter<P, S>) {
  17. super({ ...RatingFoundation.defaultAdapter, ...adapter });
  18. }
  19. init() {
  20. const { autoFocus, disabled } = this.getProps();
  21. if (autoFocus && !disabled) {
  22. this._adapter.focus();
  23. }
  24. }
  25. _getScroll(w: Window, top?: boolean) {
  26. let ret = top ? w.pageYOffset : w.pageXOffset;
  27. const method = top ? 'scrollTop' : 'scrollLeft';
  28. if (typeof ret !== 'number') {
  29. const d = w.document;
  30. // ie6,7,8 standard mode
  31. ret = d.documentElement[method];
  32. if (typeof ret !== 'number') {
  33. // quirks mode
  34. ret = d.body[method];
  35. }
  36. }
  37. return ret;
  38. }
  39. _getClientPosition(elem: Element) {
  40. let x, y;
  41. const doc = elem.ownerDocument;
  42. const { body } = doc;
  43. const docElem = doc && doc.documentElement;
  44. const box = elem.getBoundingClientRect();
  45. x = box.left;
  46. y = box.top;
  47. x -= docElem.clientLeft || body.clientLeft || 0;
  48. y -= docElem.clientTop || body.clientTop || 0;
  49. return {
  50. left: x,
  51. top: y,
  52. };
  53. }
  54. _getOffsetLeft(el: Element) {
  55. const pos = this._getClientPosition(el);
  56. const doc = el.ownerDocument;
  57. const w: Window = doc.defaultView || (doc as any).parentWindow;
  58. pos.left += this._getScroll(w);
  59. return pos.left;
  60. }
  61. getStarValue(index: number, pos: number) {
  62. const { allowHalf } = this.getProps();
  63. const direction = this._adapter.getContext('direction');
  64. const reverse = direction === 'rtl';
  65. let value = index + 1;
  66. if (allowHalf) {
  67. const starEle = this._adapter.getStarDOM(index);
  68. const leftDis = this._getOffsetLeft(starEle);
  69. const width = starEle.clientWidth;
  70. if (reverse && pos - leftDis > width / 2) {
  71. value -= 0.5;
  72. } else if (!reverse && pos - leftDis < width / 2) {
  73. value -= 0.5;
  74. }
  75. }
  76. return value;
  77. }
  78. handleHover(event: any, index: number) {
  79. const currValue = this.getStarValue(index, event.pageX);
  80. const { clearedValue, hoverValue } = this.getStates();
  81. if ((currValue !== hoverValue) && (currValue !== clearedValue)) {
  82. this._adapter.notifyHoverChange(currValue, null);
  83. }
  84. }
  85. handleMouseLeave() {
  86. this._adapter.notifyHoverChange(undefined, null);
  87. }
  88. handleClick(event: any, index: number) {
  89. const { allowClear } = this.getProps();
  90. const { value } = this.getStates();
  91. const newValue = this.getStarValue(index, event.pageX);
  92. const isReset = allowClear ? newValue === value : false;
  93. this._adapter.updateValue(isReset ? 0 : newValue);
  94. if (isReset) {
  95. this._adapter.notifyHoverChange(undefined, newValue);
  96. } else {
  97. this._adapter.clearValue(null);
  98. }
  99. }
  100. handleFocus(e: any) {
  101. this._adapter.notifyFocus(e);
  102. }
  103. handleBlur(e: any) {
  104. this._adapter.notifyBlur(e);
  105. }
  106. handleKeyDown(event: any, value: number) {
  107. const { key } = event;
  108. const { count, allowHalf } = this.getProps();
  109. const direction = this._adapter.getContext('direction');
  110. const reverse = direction === 'rtl';
  111. const step = allowHalf ? 0.5 : 1;
  112. let tempValue: number;
  113. let newValue: number;
  114. if (key === 'ArrowRight' || key === 'ArrowUp') {
  115. tempValue = value + (reverse ? - step : step);
  116. } else if (key === 'ArrowLeft' || key === 'ArrowDown') {
  117. tempValue = value + (reverse ? step : - step);
  118. }
  119. if (tempValue > count) {
  120. newValue = 0;
  121. } else if (tempValue < 0) {
  122. newValue = count;
  123. } else {
  124. newValue = tempValue;
  125. }
  126. if (['ArrowRight', 'ArrowUp', 'ArrowLeft', 'ArrowDown'].includes(key)) {
  127. this._adapter.notifyKeyDown(event);
  128. this._adapter.updateValue(newValue);
  129. this.changeFocusStar(newValue, event);
  130. event.preventDefault();
  131. this._adapter.notifyHoverChange(undefined, null);
  132. }
  133. }
  134. changeFocusStar(value: number, event: any) {
  135. const { count, allowHalf, preventScroll } = this.getProps();
  136. const index = Math.ceil(value) - 1;
  137. const starElement = [...event.currentTarget.childNodes].map(item => item.childNodes[0].childNodes);
  138. if (index < 0) {
  139. starElement[count][0].focus({ preventScroll });
  140. } else {
  141. starElement[index][allowHalf ? (value * 10 % 10 === 5 ? 0 : 1) : 0].focus({ preventScroll });
  142. }
  143. }
  144. handleStarFocusVisible = (event: any) => {
  145. const { target } = event;
  146. const { count } = this.getProps();
  147. // when rating 0 is focus visible
  148. try {
  149. if (target.matches(':focus-visible')) {
  150. this._adapter.setEmptyStarFocusVisible(true);
  151. }
  152. } catch (error) {
  153. warning(true, 'Warning: [Semi Rating] The current browser does not support the focus-visible');
  154. }
  155. }
  156. // e: FocusEvent
  157. handleStarBlur = (e: any) => {
  158. const { emptyStarFocusVisible } = this.getStates();
  159. if (emptyStarFocusVisible) {
  160. this._adapter.setEmptyStarFocusVisible(false);
  161. }
  162. }
  163. }
  164. export interface RatingItemAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  165. setFirstStarFocus: (value: boolean) => void;
  166. setSecondStarFocus: (value: boolean) => void
  167. }
  168. export class RatingItemFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<RatingItemAdapter<P, S>, P, S> {
  169. constructor(adapter: RatingItemAdapter<P, S>) {
  170. super({ ...RatingItemFoundation.defaultAdapter, ...adapter });
  171. }
  172. handleFocusVisible = (event: any, star: string) => {
  173. const { target } = event;
  174. // when rating 0 is focus visible
  175. try {
  176. if (target.matches(':focus-visible')) {
  177. if (star === 'first') {
  178. this._adapter.setFirstStarFocus(true);
  179. } else {
  180. this._adapter.setSecondStarFocus(true);
  181. }
  182. }
  183. } catch (error) {
  184. warning(true, 'Warning: [Semi Rating] The current browser does not support the focus-visible');
  185. }
  186. }
  187. // e: FocusEvent
  188. handleBlur = (e: any, star: string) => {
  189. const { firstStarFocus, secondStarFocus } = this.getStates();
  190. if (star === 'first') {
  191. firstStarFocus && this._adapter.setFirstStarFocus(false);
  192. } else {
  193. secondStarFocus && this._adapter.setSecondStarFocus(false);
  194. }
  195. }
  196. }