diff --git a/.eslintrc b/.eslintrc index 2738ee4..b6fa3ce 100644 --- a/.eslintrc +++ b/.eslintrc @@ -37,5 +37,7 @@ "import/extensions": "off", "react/jsx-filename-extension": "off", "implicit-arrow-linebreak": "off", + "no-colsole": "off", + "@typescript-eslint/no-unused-expressions": "off" } } diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index c7e88da..b63da15 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/blog/2024-11-17-event-propagation-of-react.mdx b/blog/2024-11-17-event-propagation-of-react.mdx new file mode 100644 index 0000000..36490b1 --- /dev/null +++ b/blog/2024-11-17-event-propagation-of-react.mdx @@ -0,0 +1,271 @@ +--- +slug: event-propagation-of-react +title: React의 이벤트 전파 +description: React의 이벤트 전파는 Javascript와 다르다 +keywords: + - react + - event + - event-propagation + - web +authors: HyunmoAhn +tags: [react, event, event-propagation, web, issue, trouble-shooting] +--- + + + +## Introduction +React 이벤트 핸들러 방식과 vanilla Javascript의 이벤트 핸들러 방식을 혼용해서 사용할 때 이벤트 전파가 의도한대로 동작하지 않을 수 있다. +예를 들면 아래와 같이 `button1`, `button2` 구조에서 `button2`를 클릭했을 때 `button1의` 이벤트 핸들러도 실행되는 문제가 발생한다. + +```tsx +const buttonEl = document.getElementById('button1'); +buttonEl.addEventListener('click', () => { + console.log('button1 clicked'); +}); + +const handleClick = (e) => { + e.stopPropagation(); + console.log('button2 clicked'); +}; + +return ( + + +); +``` + +``` +// When button2 is clicked +button2 clicked +button1 clicked +``` + +## Solution + +React에서 이벤트 전파는 vanilla Javascript에서 사용하는 이벤트 전파와 다르다. +React에서 이벤트 전파를 delegation 방식으로 처리하기 때문이다. ([comment](https://github.com/facebook/react/issues/13635#issuecomment-421085767)) + +React에서는 ([React 17 이후로](https://legacy.reactjs.org/blog/2020/10/20/react-v17.html#changes-to-event-delegation)) 이벤트 리스너를 rootDOM에 등록해서 사용한다. +따라서 React끼리의 event listener는 생각한 대로 DOM 구조에 따라 이벤트가 전파가 전달되지만 vanilla Javascript의 이벤트 전파는 생각대로 동작하지 않는다. + +가능하다면 두가지 이벤트를 섞어서 쓰지 않는 방향이 좋겠지만, 3rd party library를 사용하거나 제어하지 못하는 부분의 이벤트는 제어하기 힘들기 때문에 동작 방식에 맞춰 이벤트 전파를 막아야한다. + +```tsx +// Unify event listener to vanilla Javascript +const buttonEl = document.getElementById('button1'); +buttonEl.addEventListener('click', () => { + console.log('button1 clicked'); +}); + +const button2El = document.getElementById('button2'); +button2El.addEventListener('click', (e) => { + e.stopPropagation(); + console.log('button2 clicked'); +}); + +return ( + + +); + +// Or +// Unify event listener to React +const handleClick1 = (e) => { + console.log('button1 clicked'); +}; + +const handleClick = (e) => { + e.stopPropagation(); + console.log('button2 clicked'); +}; + +return ( + + +); +``` + +import { BasicEventPlayground, BothEventPlayground, BothStopEventPlayground, BothStopCapturingEventPlayground } from '@site/src/code-snippet/reactEventPropagation/index.tsx' +import { Figure } from '@site/src/components/common/Figure/index.tsx' + +## Step by Step + +:::note +이 내용은 Javascript의 Event Bubbling & Capturing을 이해하고 있다는 가정하에 설명한다.
+만약 이해가 안된다면 [MDN](https://developer.mozilla.org/ko/docs/Learn/JavaScript/Building_blocks/Event_bubbling)을 참고하자. +::: + +React의 이벤트 전파를 이해하기 위해 아래와 같은 예제를 살펴보자. +`Figure 1,2`는 각각의 방식으로 이벤트 전파를 설정하였을 때 UI를 보여준다. `#1,2,3` 아무 곳이나 클릭해보자. + + + + +`Figure 1`에서는 Vanilla Javascript의 이벤트만 설정하였기 때문에 붉은 색으로 이벤트 전파가 일어난다. +마찬가지로 `Figure 2`에서는 React의 이벤트만 설정하였기 때문에 파란 색으로 이벤트 전파가 일어난다. + +그렇다면 두 종류의 이벤트를 모두 설정한다면 어떻게 될까? `Figure 3`을 눌러보자. + + + +`Figure 3`에서는 두 종류의 이벤트를 모두 설정해두어서 이벤트가 모두 발생하고, 색은 보라색으로 표시된다. + +이벤트의 발생 순서는 다음과 같다. + +| Order | Target | Vanilla | React | Type | +| --- | --- | --- | --- | --- | +| 1 | `#1` | ✅ | ✅ | Capture | +| 2 | `#2` | ✅ | ✅ | Capture | +| 3 | `#3` | ✅ | ✅ | Capture | +| 4 | `#3` | ✅ | ✅ | Bubble | +| 5 | `#2` | ✅ | ✅ | Bubble | +| 6 | `#1` | ✅ | ✅ | Bubble | + +이벤트 전파를 막지 않았기 때문에 이벤트가 모두 발생하고, 이벤트 전파가 일어난다. + +#1 -> #2 -> #3 순으로 capturing이 발생하고 #3 -> #2 -> #1 순으로 bubbling이 발생한다. + +그렇다면, 이벤트 전파를 막으면 어떻게 될까? `Figure 4,5`를 눌러보자. + +:::note +전파를 막은 방식은 모두 동일하게 `e.stopPropagation()`으로 사용하였다. + +```tsx +// React + + +// Vanilla +button.addEventListener('click', (e) => { + e.stopPropagation(); +}); +``` + +::: + + +`Figure 4`는 `#2` 의 React Event bubbling을 막은 경우이고, `Figure 5`는 `#2`의 Vanilla Event bubbling을 막은 경우이다. + + + +두 예제의 동작은 다르게 나타난다. 결과를 정리해보자. + +
+
+

Figure 4. #2 Stop Bubbling by React

+ | Order | Target | Vanilla | React | Type | + | --- | --- | --- | --- | --- | + | 1 | `#1` | ✅ | ✅ | Capture | + | 2 | `#2` | ✅ | ✅ | Capture | + | 3 | `#3` | ✅ | ✅ | Capture | + | 4 | `#3` | ✅ | ✅ | Bubble | + | 5 | `#2` | ✅ | ✅ | Bubble | + | 6 | `#1` | ✅ | ❌ | Bubble | + +
+
+

Figure 5. #2 Stop Bubbling by Vanilla

+ + | Order | Target | Vanilla | React | Type | + | --- | --- | --- | --- | --- | + | 1 | `#1` | ✅ | ✅ | Capture | + | 2 | `#2` | ✅ | ✅ | Capture | + | 3 | `#3` | ✅ | ✅ | Capture | + | 4 | `#3` | ✅ | ❌ | Bubble | + | 5 | `#2` | ✅ | ❌ | Bubble | + | 6 | `#1` | ❌ | ❌ | Bubble | + +
+
+ +결과가 복잡 할 수 있는데, 이벤트 전파를 막은 방식과 동일한 이벤트의 결과를 살펴보자. + +`Figure 4`은 React 방식으로 전파를 막았으므로 `Figure 4`의 React 방식은 `#1`의 이벤트만 발생하지 않았고, +`Figure 5`는 Vanilla Javascript 방식으로 막았으므로 `Figure 5`의 Vanilla Javascript 방식도 `#1`의 이벤트만 발생하지 않았다. + +여기까지는 우리가 익히 아는 이벤트 전파 방식이다. 그러면 다른 방식의 이벤트 전파 결과를 살펴보자. + +`Figure 4`의 Vanilla 이벤트는 `#2`의 React bubble 전파를 막았음에도 이벤트가 발생하였다. +이와 다르게 `Figure 5`의 React 이벤트는 `#2`의 Vanilla bubble 전파를 막았는데, #1, #2, #3 모두 이벤트가 발생하지 않았다. + +여기서 중요한 사실은 React의 이벤트 핸들러 방식은 node에 직접 이벤트를 등록하지 않고, `rootDOM`에 등록하여 사용한다는 것이다. ([docs](https://legacy.reactjs.org/blog/2020/10/20/react-v17.html#changes-to-event-delegation)) + +
+ {'React +
+ +이 사실을 기반하여 `Figure 4`와 `Figure 5`의 동작이 다른지 이해 할 수 있다. + +`Figure 4`는 React 이벤트의 전파를 막았기 때문에 실제 이벤트는 `rootDOM`에서 `stopPropagation`이 동작한다. 이는 실제로는 React 내부에서 이벤트 전파를 구현했다고 표현하는 게 맞을 것 같다. +따라서 실제 이벤트는 다음과 같은 순서로 발생한다. + +1. `rootDOM`의 react event capturing +2. `#1, #2, #3`에서의 vanilla javascript event capturing +3. `#1, #2, #3`에서의 vanilla javascript event bubbling +4. `rootDOM`의 react event bubbling + +따라서 `Figure 4`에서의 이벤트는 3번, `#1, #2, #3`에서의 event bubbling을 막지 못하여서 vanilla javascript의 이벤트가 모두 발생하였다. + +그렇다면 `Figure 5`는 어떻게 될까? `#2`에서 vanilla javascript의 이벤트를 막았기 때문에, 3번의 이벤트 중 `#2`에서 이벤트 전파가 중단 되었고 `#3`과 4번, rootDOM으로의 이벤트가 발생하지 않았다. +따라서 `Figure 5`의 이벤트에서 React의 모든 bubbling 이벤트가 중단 된 것이다. + +그럼 다른 예제로 Capturing을 막는 건 어떨까? 아래 `Figure 7,8`을 살펴보자. 실행 전 동작을 예상해 보는 것도 재미있을 것 같다. +두 예제 모두 `#2`에서의 event capturing을 막았다. + + + +결과는 다음과 같다.
+ +
+
+

Figure 7. #2 Stop Capturing by React

+ | Order | Target | Vanilla | React | Type | + | --- | --- | --- | --- | --- | + | 1 | `#1` | ❌ | ✅ | Capture | + | 2 | `#2` | ❌ | ✅ | Capture | + | 3 | `#3` | ❌ | ❌ | Capture | + | 4 | `#3` | ❌ | ❌ | Bubble | + | 5 | `#2` | ❌ | ❌ | Bubble | + | 6 | `#1` | ❌ | ❌ | Bubble | + +
+
+

Figure 8. #2 Stop Capturing by Vanilla

+ + | Order | Target | Vanilla | React | Type | + | --- | --- | --- | --- | --- | + | 1 | `#1` | ✅ | ✅ | Capture | + | 2 | `#2` | ✅ | ✅ | Capture | + | 3 | `#3` | ❌ | ✅ | Capture | + | 4 | `#3` | ❌ | ❌ | Bubble | + | 5 | `#2` | ❌ | ❌ | Bubble | + | 6 | `#1` | ❌ | ❌ | Bubble | + +
+
+ +혹시 정답을 맞췄을까?
+ +`Figure 7`은 React의 capturing을 막았고, 이는 rootDOM에서 일어난다. 따라서 `#1, #2`의 react capturing은 발생했다. +하지만, vanilla javascript의 capturing은 rootDOM에서 중단되었기 때문에 이벤트가 모두 발생하지 않는다. + +`Figure 8`은 vanilla javascript의 capturing을 막았고, 이는 `#2`에서 일어난다. 따라서 `#1, #2`의 valina javascript의 capturing은 발생했다. +하지만, React의 capturing은 rootDOM에서 일어나기 때문에 `#2`에서 이벤트를 막았음에도 불구하고 capturing 이벤트가 모두 발생한다. + + +## Conclusion +React는 React에서 등록하는 이벤트를 모두 rootDOM에 등록하여 내부적으로 이벤트 전파를 처리하므로, vanilla javascript와의 이벤트 전파와는 다르게 동작한다. +일반적으로 이 내용을 자세히 알 필요는 없겠지만, React와 vanilla javascript의 이벤트를 혼용해서 처리 할 때 이벤트 전파에서 문제가 발생할 수 있다. + +이는 프로젝트 내부에서 관리하는 코드 뿐 아니라 3rd party library를 사용 할 때도 동일하게 적용되므로 예상치 못한 부분에서 이슈가 발생 할 수 있다. +필자도 React 환경에서 `swiper` 라이브러리를 사용할 때 이 이슈로 인해 문제가 발생한 적이 있어서 원인 분석을 하는 과정에서 이런 현상을 알게 되었다. + +따라서 React에서의 이벤트 등록 방식을 한번 더 인지하고 사용하는 것을 추천한다. + + +## Reference +- https://github.com/facebook/react/issues/13635#issuecomment-421085767 +- https://legacy.reactjs.org/blog/2020/10/20/react-v17.html#changes-to-event-delegation + diff --git a/docusaurus.config.js b/docusaurus.config.js index 4af533a..ec53aa8 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -135,7 +135,7 @@ module.exports = { theme: { customCss: [ require.resolve('@radix-ui/themes/styles.css'), - require.resolve('./src/css/custom.css'), + require.resolve('./src/css/custom.scss'), ], }, googleAnalytics: { diff --git a/i18n/en/docusaurus-plugin-content-blog/2024-11-17-event-propagation-of-react.mdx b/i18n/en/docusaurus-plugin-content-blog/2024-11-17-event-propagation-of-react.mdx new file mode 100644 index 0000000..c36a61b --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-blog/2024-11-17-event-propagation-of-react.mdx @@ -0,0 +1,291 @@ +--- +slug: event-propagation-of-react +title: Event Propagation in React +description: Event propagation in React differs from Javascript +keywords: + - react + - event + - event-propagation + - web +authors: HyunmoAhn +tags: [react, event, event-propagation, web, issue, trouble-shooting] +--- + + + +## Introduction +When mixing React's event handler method with vanilla Javascript's event handler method, event propagation may not work as intended. +For example, in a structure like `button1`, `button2`, clicking `button2` can trigger the event handler of `button1` as well. + +```tsx +const buttonEl = document.getElementById('button1'); +buttonEl.addEventListener('click', () => { + console.log('button1 clicked'); +}); + +const handleClick = (e) => { + e.stopPropagation(); + console.log('button2 clicked'); +}; + +return ( + + +); +``` + +``` +// When button2 is clicked +button2 clicked +button1 clicked +``` + +## Solution + +Event propagation in React differs from that in vanilla Javascript +because React handles event propagation using a delegation method. ([comment](https://github.com/facebook/react/issues/13635#issuecomment-421085767)) + +In React ([post React 17](https://legacy.reactjs.org/blog/2020/10/20/react-v17.html#changes-to-event-delegation)), +event listeners are registered on the rootDOM. +Therefore, event listeners within React propagate as expected according to the DOM structure, +but vanilla Javascript event propagation may not behave as anticipated. + +While it's best to avoid mixing the two types of events, +sometimes it's unavoidable when using third-party libraries or handling parts of the code you can't control. +In such cases, you must block event propagation according to the behavior of each method. + +```tsx +// Unify event listener to vanilla Javascript +const buttonEl = document.getElementById('button1'); +buttonEl.addEventListener('click', () => { + console.log('button1 clicked'); +}); + +const button2El = document.getElementById('button2'); +button2El.addEventListener('click', (e) => { + e.stopPropagation(); + console.log('button2 clicked'); +}); + +return ( + + +); + +// Or +// Unify event listener to React +const handleClick1 = (e) => { + console.log('button1 clicked'); +}; + +const handleClick = (e) => { + e.stopPropagation(); + console.log('button2 clicked'); +}; + +return ( + + +); +``` + +import { BasicEventPlayground, BothEventPlayground, BothStopEventPlayground, BothStopCapturingEventPlayground } from '@site/src/code-snippet/reactEventPropagation/index.tsx' +import { Figure } from '@site/src/components/common/Figure/index.tsx' + +## Step by Step + +:::note +This explanation assumes you have an understanding of Javascript's Event Bubbling & Capturing.
+If you're not familiar with it, refer to MDN. +::: + +To understand event propagation in React, let's look at the following example. +`Figure 1,2` show the UI when event propagation is set in each method. Try clicking anywhere on `#1,2,3`. + + + + +In `Figure 1`, only vanilla Javascript events are set, so the event propagates in red. +Similarly, in `Figure 2`, only React events are set, so the event propagates in blue. + +What happens if both types of events are set? Click `Figure 3`. + + + +In `Figure 3`, both types of events are set, so both events occur, and the color is indicated in purple. + +The order of event occurrence is as follows: + +| Order | Target | Vanilla | React | Type | +| --- | --- | --- | --- | --- | +| 1 | `#1` | ✅ | ✅ | Capture | +| 2 | `#2` | ✅ | ✅ | Capture | +| 3 | `#3` | ✅ | ✅ | Capture | +| 4 | `#3` | ✅ | ✅ | Bubble | +| 5 | `#2` | ✅ | ✅ | Bubble | +| 6 | `#1` | ✅ | ✅ | Bubble | + +Since event propagation wasn't blocked, all events occurred, and propagation took place. + +Capturing occurs in the order of #1 -> #2 -> #3, and bubbling occurs in the order of #3 -> #2 -> #1. + +What happens if we block event propagation? Click `Figure 4,5`. + +:::note +The method used to block propagation is the same, using e.stopPropagation(). + +```tsx +// React + + +// Vanilla +button.addEventListener('click', (e) => { + e.stopPropagation(); +}); +``` + +::: + +`Figure 4` blocks React Event bubbling on `#2`, and `Figure 5` blocks Vanilla Event bubbling on `#2`. + + + +The behavior of the two examples is different. Let's summarize the results. + +
+
+

Figure 4. #2 Stop Bubbling by React

+ | Order | Target | Vanilla | React | Type | + | --- | --- | --- | --- | --- | + | 1 | `#1` | ✅ | ✅ | Capture | + | 2 | `#2` | ✅ | ✅ | Capture | + | 3 | `#3` | ✅ | ✅ | Capture | + | 4 | `#3` | ✅ | ✅ | Bubble | + | 5 | `#2` | ✅ | ✅ | Bubble | + | 6 | `#1` | ✅ | ❌ | Bubble | + +
+
+

Figure 5. #2 Stop Bubbling by Vanilla

+ + | Order | Target | Vanilla | React | Type | + | --- | --- | --- | --- | --- | + | 1 | `#1` | ✅ | ✅ | Capture | + | 2 | `#2` | ✅ | ✅ | Capture | + | 3 | `#3` | ✅ | ✅ | Capture | + | 4 | `#3` | ✅ | ❌ | Bubble | + | 5 | `#2` | ✅ | ❌ | Bubble | + | 6 | `#1` | ❌ | ❌ | Bubble | + +
+
+ +The results might seem complex, but let's focus on the event propagation method that was blocked. + +In `Figure 4`, the React method was used to block propagation, +so only the React method in `Figure 4` did not trigger the event on `#1`. +In `Figure 5`, the Vanilla Javascript method was used to block propagation, +so only the Vanilla Javascript method in `Figure 5` did not trigger the event on `#1`. + +Up to this point, this is the event propagation method we are familiar with. +Now, let's look at the results of the other method's event propagation. + +In `Figure 4`, the Vanilla event occurred even though the React bubble propagation on `#2` was blocked. +Conversely, in `Figure 5`, the React event did not occur on #1, #2, or #3, +even though the Vanilla bubble propagation on `#2` was blocked. + +The key point here is that React's event handler method doesn't directly register events on nodes +but uses event delegation by registering them on the `rootDOM` ([docs](https://legacy.reactjs.org/blog/2020/10/20/react-v17.html#changes-to-event-delegation)) + +
+ {'React +
+ +Based on this fact, we can understand why the behavior of `Figure 4` and `Figure 5` differs. + +In `Figure 4`, React event propagation was blocked, so the actual event propagation occurs at the `rootDOM`. +This means the event propagation is actually implemented within React. +Therefore, the actual event occurs in the following order: + +1. React event capturing at `rootDOM` +2. Vanilla javascript event capturing at `#1, #2, #3` +3. Vanilla javascript event bubbling at `#1, #2, #3` +4. React event bubbling at `rootDOM` + +Therefore, in `Figure 4`, the event propagation wasn't blocked during the 3rd step, the bubbling at `#1, #2, #3`, +so all vanilla javascript events occurred. + +So what about `Figure 5`? +Since vanilla javascript's event was blocked at `#2`, +the event propagation was interrupted on `#2` during the 3rd step, +and no events occurred at `#3` or during the 4th step, at the rootDOM. +Therefore, all React bubbling events in `Figure 5` were interrupted. + +What about blocking Capturing in another example? +Let's look at `Figure 7,8`. It might be interesting to predict the behavior before running it. +Both examples blocked event capturing at `#2`. + + + + +The results are as follows.
+ +
+
+

Figure 7. #2 Stop Capturing by React

+ | Order | Target | Vanilla | React | Type | + | --- | --- | --- | --- | --- | + | 1 | `#1` | ❌ | ✅ | Capture | + | 2 | `#2` | ❌ | ✅ | Capture | + | 3 | `#3` | ❌ | ❌ | Capture | + | 4 | `#3` | ❌ | ❌ | Bubble | + | 5 | `#2` | ❌ | ❌ | Bubble | + | 6 | `#1` | ❌ | ❌ | Bubble | + +
+
+

Figure 8. #2 Stop Capturing by Vanilla

+ + | Order | Target | Vanilla | React | Type | + | --- | --- | --- | --- | --- | + | 1 | `#1` | ✅ | ✅ | Capture | + | 2 | `#2` | ✅ | ✅ | Capture | + | 3 | `#3` | ❌ | ✅ | Capture | + | 4 | `#3` | ❌ | ❌ | Bubble | + | 5 | `#2` | ❌ | ❌ | Bubble | + | 6 | `#1` | ❌ | ❌ | Bubble | + +
+
+ +Did you get the answer right?
+ +In `Figure 7`, React capturing was blocked, which occurs at the rootDOM. +Therefore, React capturing occurred at `#1, #2`. +However, vanilla javascript capturing was interrupted at the rootDOM, so no events occurred. + +In `Figure 8`, vanilla javascript capturing was blocked, which occurs at `#2`. +Therefore, vanilla javascript capturing occurred at `#1, #2`. +However, React capturing occurs at the rootDOM, so even though the event was blocked at `#2`, +all capturing events occurred. + +## Conclusion +React registers events on the rootDOM and handles event propagation internally, +so it behaves differently from vanilla javascript event propagation. +Generally, there's no need to know this in detail, +but when mixing React and vanilla javascript events, issues can arise in event propagation. + +This applies not only to code managed within the project +but also when using third-party libraries, so unexpected issues can occur. +The author encountered this issue when using the swiper library in a React environment, +leading to this discovery during root cause analysis. + +Therefore, it's recommended to be aware of React's event registration method when using it. + +## Reference +- https://github.com/facebook/react/issues/13635#issuecomment-421085767 +- https://legacy.reactjs.org/blog/2020/10/20/react-v17.html#changes-to-event-delegation + diff --git a/package.json b/package.json index cc526fe..823af37 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,10 @@ "husky": "^8.0.3", "lint-staged": "^13.2.3", "prettier": "^3.0.1", + "stylelint": "^16.10.0", + "stylelint-config-prettier-scss": "^1.0.0", + "stylelint-config-standard-scss": "^13.1.0", + "stylelint-prettier": "^5.0.2", "typescript": "^5.6.3" }, "browserslist": { diff --git a/src/code-snippet/reactEventPropagation/EventPlayground/EventPlayground.module.scss b/src/code-snippet/reactEventPropagation/EventPlayground/EventPlayground.module.scss new file mode 100644 index 0000000..c7ffde1 --- /dev/null +++ b/src/code-snippet/reactEventPropagation/EventPlayground/EventPlayground.module.scss @@ -0,0 +1,44 @@ +.container { + display: flex; + flex-direction: column; + position: relative; + padding: 30px; + border: 1px solid #ccc; + border-radius: 5px; + cursor: pointer; + overflow: hidden; + background-color: var(--docusaurus-highlighted-code-line-bg); + width: 100%; +} + +.highlightContainer { + display: flex; + justify-content: flex-start; + align-items: flex-start; + gap: 5px; + margin: 5px; + margin-bottom: 20px; + min-height: 25px; +} + +.highlight { + display: flex; + pointer-events: none; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + + &.react { + background-color: color-mix(in srgb, var(--color-blue) 70%, transparent); + } + + &.vanilla { + background-color: color-mix(in srgb, var(--color-red) 50%, transparent); + } +} + +.text { + z-index: 1; +} diff --git a/src/code-snippet/reactEventPropagation/EventPlayground/index.tsx b/src/code-snippet/reactEventPropagation/EventPlayground/index.tsx new file mode 100644 index 0000000..71d70ad --- /dev/null +++ b/src/code-snippet/reactEventPropagation/EventPlayground/index.tsx @@ -0,0 +1,148 @@ +import { + useId, + useEffect, + useState, + ReactNode, + MouseEvent, + createContext, + useContext, +} from 'react'; +import cx from 'classnames'; +import style from './EventPlayground.module.scss'; +import { timeout, DELAY, createController } from './utils'; + +export const ReactEventPlaygroundContext = createContext(createController()); +export const VanillaEventPlaygroundContext = createContext(createController()); +export const EventPlaygroundProvider = ({ children }: { children: ReactNode }) => ( + + + {children} + + +); + +interface EventPlaygroundProps { + label?: string; + children?: ReactNode; + reset?: boolean; + skipReact?: boolean; + skipVanilla?: boolean; + reactStopBubble?: boolean; + reactStopCapture?: boolean; + vanillaStopBubble?: boolean; + vanillaStopCapture?: boolean; +} + +export const EventPlayground = ({ + label, + children, + reset, + skipReact, + skipVanilla, + reactStopBubble, + reactStopCapture, + vanillaStopBubble, + vanillaStopCapture, +}: EventPlaygroundProps) => { + const id = useId(); + const reactEventController = useContext(ReactEventPlaygroundContext); + const vanillaEventController = useContext(VanillaEventPlaygroundContext); + const [reactEvent, setReactEvent] = useState(false); + const [vanillaEvent, setVanillaEvent] = useState(false); + + const handleReactClick = () => { + if (!skipReact) { + reactEventController.register(() => { + setReactEvent(true); + }); + reactEventController.register(async () => { + await timeout(DELAY); + }); + reactEventController.register(() => { + setReactEvent(false); + }); + } + }; + + const handleReactClickCapture = (e: MouseEvent) => { + if (reset) { + reactEventController.clear(); + } + reactStopCapture && e.stopPropagation(); + handleReactClick(); + }; + + const handleReactClickBubble = (e: MouseEvent) => { + reactStopBubble && e.stopPropagation(); + handleReactClick(); + }; + + useEffect(() => { + const handleVanillaClick = (e: Event) => { + if (!skipVanilla) { + vanillaEventController.register(() => { + setVanillaEvent(true); + }); + vanillaEventController.register(async () => { + await timeout(DELAY); + }); + vanillaEventController.register(() => { + setVanillaEvent(false); + }); + } + }; + const handleVanillaClickBubble = (e: Event) => { + vanillaStopBubble && e.stopPropagation(); + handleVanillaClick(e); + }; + + const handleVanillaClickCapture = (e: Event) => { + if (reset) { + vanillaEventController.clear(); + } + vanillaStopCapture && e.stopPropagation(); + handleVanillaClick(e); + }; + + const button = document.getElementById(id); + + button.addEventListener('click', handleVanillaClickCapture, { capture: true }); + button.addEventListener('click', handleVanillaClickBubble, { capture: false }); + return () => { + button.removeEventListener('click', handleVanillaClickCapture, { + capture: true, + }); + button.removeEventListener('click', handleVanillaClickBubble, { + capture: false, + }); + }; + }, [id, reset, skipVanilla, vanillaStopBubble, vanillaStopCapture]); + + return ( + // eslint-disable-next-line max-len + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions +
+

{label}

+
+ {reactEvent && ( + <> +
+ React Event + + )} + {vanillaEvent && ( + <> +
+
Vanilla Event
+ + )} +
+ {children} +
+ ); +}; diff --git a/src/code-snippet/reactEventPropagation/EventPlayground/utils.ts b/src/code-snippet/reactEventPropagation/EventPlayground/utils.ts new file mode 100644 index 0000000..b3daee2 --- /dev/null +++ b/src/code-snippet/reactEventPropagation/EventPlayground/utils.ts @@ -0,0 +1,34 @@ +export const DELAY = 600; + +export function timeout(ns: number) { + return new Promise((resolve) => { + setTimeout(resolve, ns); + }); +} + +export const createController = () => { + let events: (() => void)[] = []; + const processor = eventProcessor(); + + const register = async (cb: () => void) => { + events.push(cb); + await processor.next(); + }; + + async function* eventProcessor() { + while (true) { + const event = events.shift(); + yield event?.(); + } + } + + const clear = () => { + events.forEach((event) => event()); + events = []; + }; + + return { + register, + clear, + }; +}; diff --git a/src/code-snippet/reactEventPropagation/index.module.scss b/src/code-snippet/reactEventPropagation/index.module.scss new file mode 100644 index 0000000..222721a --- /dev/null +++ b/src/code-snippet/reactEventPropagation/index.module.scss @@ -0,0 +1,7 @@ +.basicEventPlayground { + display: flex; + width: 100%; + justify-content: space-around; + column-gap: 30px; + flex-wrap: wrap; +} diff --git a/src/code-snippet/reactEventPropagation/index.tsx b/src/code-snippet/reactEventPropagation/index.tsx new file mode 100644 index 0000000..9917618 --- /dev/null +++ b/src/code-snippet/reactEventPropagation/index.tsx @@ -0,0 +1,97 @@ +import { Figure } from '../../components/common/Figure'; +import { EventPlayground, EventPlaygroundProvider } from './EventPlayground'; +import style from './index.module.scss'; + +export { EventPlayground } from './EventPlayground'; + +export const BasicEventPlayground = () => { + return ( +
+
+ + + + + + + +
+ +
+ + + + + + + +
+
+ ); +}; + +export const BothEventPlayground = () => { + return ( +
+ + + + + + + +
+ ); +}; + +export const BothStopEventPlayground = () => { + return ( +
+
+ + + + + + + +
+ +
+ + + + + + + +
+
+ ); +}; + +export const BothStopCapturingEventPlayground = () => { + return ( +
+
+ + + + + + + +
+ +
+ + + + + + + +
+
+ ); +}; diff --git a/src/components/common/Figure/Figure.module.scss b/src/components/common/Figure/Figure.module.scss new file mode 100644 index 0000000..357ced2 --- /dev/null +++ b/src/components/common/Figure/Figure.module.scss @@ -0,0 +1,28 @@ +.figureContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + column-gap: 5px; + padding: 10px; + flex: 1; + + .titleContainer { + display: flex; + justify-content: center; + align-items: center; + column-gap: 10px; + cursor: default; + margin: 10px 0 15px; + + .indexing { + font-weight: 700; + color: var(--color-gray-darker); + } + + .title { + font-style: italic; + color: var(--ifm-color-primary-light); + } + } +} diff --git a/src/components/common/Figure/index.tsx b/src/components/common/Figure/index.tsx new file mode 100644 index 0000000..8b77721 --- /dev/null +++ b/src/components/common/Figure/index.tsx @@ -0,0 +1,20 @@ +import { ReactNode } from 'react'; +import style from './Figure.module.scss'; + +export interface FigureProps { + index: number; + title: string; + children: ReactNode; +} + +export const Figure = ({ index, title, children }: FigureProps) => { + return ( +
+ {children} +
+ Figure {index} + {title} +
+
+ ); +}; diff --git a/src/css/color.scss b/src/css/color.scss new file mode 100644 index 0000000..873e0de --- /dev/null +++ b/src/css/color.scss @@ -0,0 +1,115 @@ +:root { + /* White and Black */ + --color-white: #fff; + --color-black: #000; + + /* Gray Palette */ + --color-gray-lightest: #f5f5f5; + --color-gray-lighter: #eee; + --color-gray-light: #e0e0e0; + --color-gray: #9e9e9e; + --color-gray-dark: #757575; + --color-gray-darker: #616161; + --color-gray-darkest: #424242; + + /* Red Palette */ + --color-red-lightest: #ffebee; + --color-red-lighter: #ffcdd2; + --color-red-light: #ef9a9a; + --color-red: #f44336; + --color-red-dark: #e53935; + --color-red-darker: #d32f2f; + --color-red-darkest: #b71c1c; + + /* Green Palette */ + --color-green-lightest: #e8f5e9; + --color-green-lighter: #c8e6c9; + --color-green-light: #a5d6a7; + --color-green: #4caf50; + --color-green-dark: #43a047; + --color-green-darker: #388e3c; + --color-green-darkest: #1b5e20; + + /* Yellow Palette */ + --color-yellow-lightest: #fffde7; + --color-yellow-lighter: #fff9c4; + --color-yellow-light: #fff59d; + --color-yellow: #ffeb3b; + --color-yellow-dark: #fdd835; + --color-yellow-darker: #fbc02d; + --color-yellow-darkest: #f57f17; + + /* Blue Palette */ + --color-blue-lightest: #e3f2fd; + --color-blue-lighter: #bbdefb; + --color-blue-light: #90caf9; + --color-blue: #2196f3; + --color-blue-dark: #1e88e5; + --color-blue-darker: #1976d2; + --color-blue-darkest: #0d47a1; + + /* Primary Color */ + --ifm-color-primary: #286242; + --ifm-color-primary-dark: #24583b; + --ifm-color-primary-darker: #225338; + --ifm-color-primary-darkest: #1c452e; + --ifm-color-primary-light: #2c6c49; + --ifm-color-primary-lighter: #2e714c; + --ifm-color-primary-lightest: #347f56; +} + +[data-theme='dark'] { + /* Primary Color */ + --ifm-color-primary: #23fbcc; + --ifm-color-primary-dark: #07fac5; + --ifm-color-primary-darker: #04efbc; + --ifm-color-primary-darkest: #04c59b; + --ifm-color-primary-light: #3ffcd3; + --ifm-color-primary-lighter: #4dfcd6; + --ifm-color-primary-lightest: #77fde0; + + /* Gray Palette */ + --color-gray-lightest: #424242; + --color-gray-lighter: #616161; + --color-gray-light: #757575; + --color-gray: #9e9e9e; + --color-gray-dark: #e0e0e0; + --color-gray-darker: #eee; + --color-gray-darkest: #f5f5f5; + + /* Red Palette */ + --color-red-lightest: #b71c1c; + --color-red-lighter: #d32f2f; + --color-red-light: #e53935; + --color-red: #f44336; + --color-red-dark: #ef9a9a; + --color-red-darker: #ffcdd2; + --color-red-darkest: #ffebee; + + /* Green Palette */ + --color-green-lightest: #1b5e20; + --color-green-lighter: #388e3c; + --color-green-light: #43a047; + --color-green: #4caf50; + --color-green-dark: #a5d6a7; + --color-green-darker: #c8e6c9; + --color-green-darkest: #e8f5e9; + + /* Yellow Palette */ + --color-yellow-lightest: #f57f17; + --color-yellow-lighter: #fbc02d; + --color-yellow-light: #fdd835; + --color-yellow: #ffeb3b; + --color-yellow-dark: #fff59d; + --color-yellow-darker: #fff9c4; + --color-yellow-darkest: #fffde7; + + /* Blue Palette */ + --color-blue-lightest: #0d47a1; + --color-blue-lighter: #1976d2; + --color-blue-light: #1e88e5; + --color-blue: #2196f3; + --color-blue-dark: #90caf9; + --color-blue-darker: #bbdefb; + --color-blue-darkest: #e3f2fd; +} diff --git a/src/css/custom.css b/src/css/custom.scss similarity index 69% rename from src/css/custom.css rename to src/css/custom.scss index 0be5ffa..a1a276f 100644 --- a/src/css/custom.css +++ b/src/css/custom.scss @@ -1,3 +1,5 @@ +@use './color'; + /** * Any CSS included here will be global. The classic template * bundles Infima by default. Infima is a CSS framework designed to @@ -6,26 +8,17 @@ /* You can override the default Infima variables here. */ :root { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: rgb(33, 175, 144); - --ifm-color-primary-darker: rgb(31, 165, 136); - --ifm-color-primary-darkest: rgb(26, 136, 112); - --ifm-color-primary-light: rgb(70, 203, 174); - --ifm-color-primary-lighter: rgb(102, 212, 189); - --ifm-color-primary-lightest: rgb(146, 224, 208); --ifm-code-font-size: 95%; - - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + --docusaurus-highlighted-code-line-bg: rgb(var(--color-black, 0.1)); --docusaurus-error-code-line-bg: #ffccc080; --docusaurus-good-code-line-bg: #25a38940; - --ifm-container-width-xl: 1520px; } /* If you have a different syntax highlighting theme for dark mode. */ [data-theme='dark'] { /* Color which works with dark mode syntax highlighting theme */ - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + --docusaurus-highlighted-code-line-bg: rgb(0 0 0 / 0.3); --docusaurus-error-code-line-bg: #cc333340; --docusaurus-good-code-line-bg: #20d8a840; } @@ -33,7 +26,7 @@ .theme-code-block-highlighted-line { background-color: var(--docusaurus-highlighted-code-line-bg); display: block; - margin: 0 calc(var(--ifm-pre-padding)*-1); + margin: 0 calc(var(--ifm-pre-padding) * -1); padding: 0 var(--ifm-pre-padding); } @@ -51,10 +44,10 @@ display: block; margin: 0 calc(-1 * var(--ifm-pre-padding)); padding: 0 var(--ifm-pre-padding); - border-left: 3px solid rgb(37, 194, 160); + border-left: 3px solid rgb(37 194 160); font-weight: 600; } .radix-themes { - color: var(--ifm-font-color-base); + color: var(--ifm-font-color-base); } diff --git a/static/assets/event-propagation-of-react/react-event-handle.png b/static/assets/event-propagation-of-react/react-event-handle.png new file mode 100644 index 0000000..a32da42 Binary files /dev/null and b/static/assets/event-propagation-of-react/react-event-handle.png differ diff --git a/stylelint.config.js b/stylelint.config.js new file mode 100644 index 0000000..5fd729e --- /dev/null +++ b/stylelint.config.js @@ -0,0 +1,45 @@ +/** @type {import('stylelint').Config} */ +module.exports = { + extends: [ + 'stylelint-config-standard-scss', + 'stylelint-config-prettier-scss', + 'stylelint-prettier/recommended', + ], + rules: { + 'selector-class-pattern': null, + 'declaration-block-no-redundant-longhand-properties': null, + 'value-keyword-case': null, + 'alpha-value-notation': 'number', + 'no-descending-specificity': null, + 'custom-property-pattern': null, + 'function-name-case': null, + 'font-family-no-missing-generic-family-keyword': [ + true, + { + ignoreFontFamilies: ['apple-system-wrapper'], + }, + ], + 'property-no-vendor-prefix': null, + 'value-no-vendor-prefix': null, + 'scss/no-global-function-names': null, + 'scss/at-mixin-pattern': null, + 'scss/dollar-variable-pattern': null, + 'selector-id-pattern': null, + 'scss/at-function-pattern': null, + 'scss/percent-placeholder-pattern': null, + 'no-duplicate-selectors': null, + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global', 'local'], + }, + ], + 'media-feature-name-no-unknown': [ + true, + { + ignoreMediaFeatureNames: ['variables'], + }, + ], + 'media-feature-range-notation': null, + }, +}; diff --git a/yarn.lock b/yarn.lock index 22a4477..d390062 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3403,6 +3403,41 @@ __metadata: languageName: node linkType: hard +"@csstools/css-parser-algorithms@npm:^3.0.1": + version: 3.0.4 + resolution: "@csstools/css-parser-algorithms@npm:3.0.4" + peerDependencies: + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd + languageName: node + linkType: hard + +"@csstools/css-tokenizer@npm:^3.0.1": + version: 3.0.3 + resolution: "@csstools/css-tokenizer@npm:3.0.3" + checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5 + languageName: node + linkType: hard + +"@csstools/media-query-list-parser@npm:^3.0.1": + version: 3.0.1 + resolution: "@csstools/media-query-list-parser@npm:3.0.1" + peerDependencies: + "@csstools/css-parser-algorithms": ^3.0.1 + "@csstools/css-tokenizer": ^3.0.1 + checksum: 10c0/fca1935cabf9fb94128da87f72c34aa2cfce8eb0beba4c78d685c7b42aaba3521067710afc6905b7347fc41fe53947536ce15a7ef3387b48763d8f7d71778d5e + languageName: node + linkType: hard + +"@csstools/selector-specificity@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/selector-specificity@npm:4.0.0" + peerDependencies: + postcss-selector-parser: ^6.1.0 + checksum: 10c0/6f4d4ecfdcd37f950100de8ffe0b4c1b1cc8c004aab2c2ebaa5c3e2bca2412d15b17d4628435f47a62d2c56db41bcbf985cb9c69e74b89964d48e421e93e75ba + languageName: node + linkType: hard + "@discoveryjs/json-ext@npm:0.5.7": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" @@ -4013,6 +4048,13 @@ __metadata: languageName: node linkType: hard +"@dual-bundle/import-meta-resolve@npm:^4.1.0": + version: 4.1.0 + resolution: "@dual-bundle/import-meta-resolve@npm:4.1.0" + checksum: 10c0/55069e550ee2710e738dd8bbd34aba796cede456287454b50c3be46fbef8695d00625677f3f41f5ffbec1174c0f57f314da9a908388bc9f8ad41a8438db884d9 + languageName: node + linkType: hard + "@emnapi/runtime@npm:^1.2.0": version: 1.3.1 resolution: "@emnapi/runtime@npm:1.3.1" @@ -7687,6 +7729,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^8.0.1": + version: 8.17.1 + resolution: "ajv@npm:8.17.1" + dependencies: + fast-deep-equal: "npm:^3.1.3" + fast-uri: "npm:^3.0.1" + json-schema-traverse: "npm:^1.0.0" + require-from-string: "npm:^2.0.2" + checksum: 10c0/ec3ba10a573c6b60f94639ffc53526275917a2df6810e4ab5a6b959d87459f9ef3f00d5e7865b82677cb7d21590355b34da14d1d0b9c32d75f95a187e76fff35 + languageName: node + linkType: hard + "algoliasearch-helper@npm:^3.13.3": version: 3.22.5 resolution: "algoliasearch-helper@npm:3.22.5" @@ -8167,6 +8221,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^2.0.0": + version: 2.0.0 + resolution: "balanced-match@npm:2.0.0" + checksum: 10c0/60a54e0b75a61674e16a7a336b805f06c72d6f8fc457639c24efc512ba2bf9cb5744b9f6f5225afcefb99da39714440c83c737208cc65c5d9ecd1f3093331ca3 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -9231,6 +9292,23 @@ __metadata: languageName: node linkType: hard +"cosmiconfig@npm:^9.0.0": + version: 9.0.0 + resolution: "cosmiconfig@npm:9.0.0" + dependencies: + env-paths: "npm:^2.2.1" + import-fresh: "npm:^3.3.0" + js-yaml: "npm:^4.1.0" + parse-json: "npm:^5.2.0" + peerDependencies: + typescript: ">=4.9.5" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/1c1703be4f02a250b1d6ca3267e408ce16abfe8364193891afc94c2d5c060b69611fdc8d97af74b7e6d5d1aac0ab2fb94d6b079573146bc2d756c2484ce5f0ee + languageName: node + linkType: hard + "crelt@npm:^1.0.5": version: 1.0.6 resolution: "crelt@npm:1.0.6" @@ -9281,6 +9359,13 @@ __metadata: languageName: node linkType: hard +"css-functions-list@npm:^3.2.3": + version: 3.2.3 + resolution: "css-functions-list@npm:3.2.3" + checksum: 10c0/03f9ed34eeed310d2b1cf0e524eea02bc5f87854a4de85f8957ea432ab1036841a3fb00879590519f7bb8fda40d992ce7a72fa9b61696ca1dc53b90064858f96 + languageName: node + linkType: hard + "css-loader@npm:^6.8.1": version: 6.11.0 resolution: "css-loader@npm:6.11.0" @@ -9381,6 +9466,16 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:^3.0.0, css-tree@npm:^3.0.1": + version: 3.0.1 + resolution: "css-tree@npm:3.0.1" + dependencies: + mdn-data: "npm:2.12.1" + source-map-js: "npm:^1.0.1" + checksum: 10c0/9f117f3067e68e9edb0b3db0134f420db1a62bede3e84d8835767ecfaa6f8ced5e87989cf39b65ffe65d788c134c8ea9abd7393d7c35838a9da84326adf57a9b + languageName: node + linkType: hard + "css-tree@npm:~2.2.0": version: 2.2.1 resolution: "css-tree@npm:2.2.1" @@ -10479,7 +10574,7 @@ __metadata: languageName: node linkType: hard -"env-paths@npm:^2.2.0": +"env-paths@npm:^2.2.0, env-paths@npm:^2.2.1": version: 2.2.1 resolution: "env-paths@npm:2.2.1" checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 @@ -11231,6 +11326,13 @@ __metadata: languageName: node linkType: hard +"fast-diff@npm:^1.1.2": + version: 1.3.0 + resolution: "fast-diff@npm:1.3.0" + checksum: 10c0/5c19af237edb5d5effda008c891a18a585f74bf12953be57923f17a3a4d0979565fc64dbc73b9e20926b9d895f5b690c618cbb969af0cf022e3222471220ad29 + languageName: node + linkType: hard + "fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0": version: 3.3.1 resolution: "fast-glob@npm:3.3.1" @@ -11271,6 +11373,20 @@ __metadata: languageName: node linkType: hard +"fast-uri@npm:^3.0.1": + version: 3.0.3 + resolution: "fast-uri@npm:3.0.3" + checksum: 10c0/4b2c5ce681a062425eae4f15cdc8fc151fd310b2f69b1f96680677820a8b49c3cd6e80661a406e19d50f0c40a3f8bffdd458791baf66f4a879d80be28e10a320 + languageName: node + linkType: hard + +"fastest-levenshtein@npm:^1.0.16": + version: 1.0.16 + resolution: "fastest-levenshtein@npm:1.0.16" + checksum: 10c0/7e3d8ae812a7f4fdf8cad18e9cde436a39addf266a5986f653ea0d81e0de0900f50c0f27c6d5aff3f686bcb48acbd45be115ae2216f36a6a13a7dbbf5cad878b + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -11316,6 +11432,15 @@ __metadata: languageName: node linkType: hard +"file-entry-cache@npm:^9.1.0": + version: 9.1.0 + resolution: "file-entry-cache@npm:9.1.0" + dependencies: + flat-cache: "npm:^5.0.0" + checksum: 10c0/4b4dbc1e972f50202b1a4430d30fd99378ef6e2a64857176abdc65c5e4730a948fb37e274478520a7bacbc70f3abba455a4b9d2c1915c53f30d11dc85d3fef5e + languageName: node + linkType: hard + "file-loader@npm:^6.2.0": version: 6.2.0 resolution: "file-loader@npm:6.2.0" @@ -11426,6 +11551,16 @@ __metadata: languageName: node linkType: hard +"flat-cache@npm:^5.0.0": + version: 5.0.0 + resolution: "flat-cache@npm:5.0.0" + dependencies: + flatted: "npm:^3.3.1" + keyv: "npm:^4.5.4" + checksum: 10c0/847f25eefec5d6614fdce76dc6097ee98f63fd4dfbcb908718905ac56610f939f4c28b1f908d6e8857d49286fe73235095d2e7ac9df096c35a3e8a15204c361b + languageName: node + linkType: hard + "flat@npm:^5.0.2": version: 5.0.2 resolution: "flat@npm:5.0.2" @@ -11442,6 +11577,13 @@ __metadata: languageName: node linkType: hard +"flatted@npm:^3.3.1": + version: 3.3.1 + resolution: "flatted@npm:3.3.1" + checksum: 10c0/324166b125ee07d4ca9bcf3a5f98d915d5db4f39d711fba640a3178b959919aae1f7cfd8aabcfef5826ed8aa8a2aa14cc85b2d7d18ff638ddf4ae3df39573eaf + languageName: node + linkType: hard + "follow-redirects@npm:^1.0.0": version: 1.15.2 resolution: "follow-redirects@npm:1.15.2" @@ -11874,6 +12016,13 @@ __metadata: languageName: node linkType: hard +"globjoin@npm:^0.1.4": + version: 0.1.4 + resolution: "globjoin@npm:0.1.4" + checksum: 10c0/236e991b48f1a9869fe2aa7bb5141fb1f32973940567a3c012f8ccb58c3c85ab78ce594d374fa819410fff3b48cfd24584d7ef726939f8a3c3772890e62ea16b + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -12486,6 +12635,10 @@ __metadata: react-dom: "npm:^18.2.0" sass: "npm:^1.80.5" styled-components: "npm:^6.0.7" + stylelint: "npm:^16.10.0" + stylelint-config-prettier-scss: "npm:^1.0.0" + stylelint-config-standard-scss: "npm:^13.1.0" + stylelint-prettier: "npm:^5.0.2" typescript: "npm:^5.6.3" url-loader: "npm:^4.1.1" languageName: unknown @@ -12539,6 +12692,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^6.0.2": + version: 6.0.2 + resolution: "ignore@npm:6.0.2" + checksum: 10c0/9a38feac1861906a78ba0f03e8ef3cd6b0526dce2a1a84e1009324b557763afeb9c3ebcc04666b21f7bbf71adda45e76781bb9e2eaa0903d45dcaded634454f5 + languageName: node + linkType: hard + "image-size@npm:^1.0.2": version: 1.1.1 resolution: "image-size@npm:1.1.1" @@ -13037,6 +13197,13 @@ __metadata: languageName: node linkType: hard +"is-plain-object@npm:^5.0.0": + version: 5.0.0 + resolution: "is-plain-object@npm:5.0.0" + checksum: 10c0/893e42bad832aae3511c71fd61c0bf61aa3a6d853061c62a307261842727d0d25f761ce9379f7ba7226d6179db2a3157efa918e7fe26360f3bf0842d9f28942c + languageName: node + linkType: hard + "is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -13454,7 +13621,7 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.5.3": +"keyv@npm:^4.5.3, keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" dependencies: @@ -13498,6 +13665,20 @@ __metadata: languageName: node linkType: hard +"known-css-properties@npm:^0.34.0": + version: 0.34.0 + resolution: "known-css-properties@npm:0.34.0" + checksum: 10c0/8549969f02b1858554e89faf4548ece37625d0d21b42e8d54fa53184e68e1512ef2531bb15941575ad816361ab7447b598c1b18c1b96ce0a868333d1a68f2e2c + languageName: node + linkType: hard + +"known-css-properties@npm:^0.35.0": + version: 0.35.0 + resolution: "known-css-properties@npm:0.35.0" + checksum: 10c0/04a4a2859d62670bb25b5b28091a1f03f6f0d3298a5ed3e7476397c5287b98c434f6dd9c004a0c67a53b7f21acc93f83c972e98c122f568d4d0bd21fd2b90fb6 + languageName: node + linkType: hard + "ky@npm:^1.2.0": version: 1.7.2 resolution: "ky@npm:1.7.2" @@ -13733,6 +13914,13 @@ __metadata: languageName: node linkType: hard +"lodash.truncate@npm:^4.4.2": + version: 4.4.2 + resolution: "lodash.truncate@npm:4.4.2" + checksum: 10c0/4e870d54e8a6c86c8687e057cec4069d2e941446ccab7f40b4d9555fa5872d917d0b6aa73bece7765500a3123f1723bcdba9ae881b679ef120bba9e1a0b0ed70 + languageName: node + linkType: hard + "lodash.uniq@npm:^4.5.0": version: 4.5.0 resolution: "lodash.uniq@npm:4.5.0" @@ -13899,6 +14087,13 @@ __metadata: languageName: node linkType: hard +"mathml-tag-names@npm:^2.1.3": + version: 2.1.3 + resolution: "mathml-tag-names@npm:2.1.3" + checksum: 10c0/e2b094658a2618433efd2678a5a3e551645e09ba17c7c777783cd8dfa0178b0195fda0a5c46a6be5e778923662cf8dde891c894c869ff14fbb4ea3208c31bc4d + languageName: node + linkType: hard + "mdast-util-directive@npm:^3.0.0": version: 3.0.0 resolution: "mdast-util-directive@npm:3.0.0" @@ -14194,6 +14389,20 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.12.1": + version: 2.12.1 + resolution: "mdn-data@npm:2.12.1" + checksum: 10c0/1a09f441bdd423f2b0ab712665a1a3329fe7b15e9a2dad8c1c10c521ddb204ed186e7ac91052fd53a5ae0a07ac6eae53b5bcbb59ba8a1fb654268611297eea4a + languageName: node + linkType: hard + +"mdn-data@npm:^2.12.2": + version: 2.12.2 + resolution: "mdn-data@npm:2.12.2" + checksum: 10c0/b22443b71d70f72ccc3c6ba1608035431a8fc18c3c8fc53523f06d20e05c2ac10f9b53092759a2ca85cf02f0d37036f310b581ce03e7b99ac74d388ef8152ade + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -14210,6 +14419,13 @@ __metadata: languageName: node linkType: hard +"meow@npm:^13.2.0": + version: 13.2.0 + resolution: "meow@npm:13.2.0" + checksum: 10c0/d5b339ae314715bcd0b619dd2f8a266891928e21526b4800d49b4fba1cc3fff7e2c1ff5edd3344149fac841bc2306157f858e8c4d5eaee4d52ce52ad925664ce + languageName: node + linkType: hard + "merge-descriptors@npm:1.0.1": version: 1.0.1 resolution: "merge-descriptors@npm:1.0.1" @@ -14968,7 +15184,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": +"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5, micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -16002,7 +16218,7 @@ __metadata: languageName: node linkType: hard -"picocolors@npm:^1.0.1, picocolors@npm:^1.1.0": +"picocolors@npm:^1.0.1, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 @@ -16180,6 +16396,13 @@ __metadata: languageName: node linkType: hard +"postcss-media-query-parser@npm:^0.2.3": + version: 0.2.3 + resolution: "postcss-media-query-parser@npm:0.2.3" + checksum: 10c0/252c8cf24f0e9018516b0d70b7b3d6f5b52e81c4bab2164b49a4e4c1b87bb11f5dbe708c0076990665cb24c70d5fd2f3aee9c922b0f67c7c619e051801484688 + languageName: node + linkType: hard + "postcss-merge-idents@npm:^6.0.3": version: 6.0.3 resolution: "postcss-merge-idents@npm:6.0.3" @@ -16454,7 +16677,32 @@ __metadata: languageName: node linkType: hard -"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.16": +"postcss-resolve-nested-selector@npm:^0.1.6": + version: 0.1.6 + resolution: "postcss-resolve-nested-selector@npm:0.1.6" + checksum: 10c0/84213a2bccce481b1569c595a3c547b25c6ef1cca839fbd6c82c12ab407559966e968613c7454b573aa54f38cfd7e900c1fd603f0efc9f51939ab9f93f115455 + languageName: node + linkType: hard + +"postcss-safe-parser@npm:^7.0.1": + version: 7.0.1 + resolution: "postcss-safe-parser@npm:7.0.1" + peerDependencies: + postcss: ^8.4.31 + checksum: 10c0/6957b10b818bd8d4664ec0e548af967f7549abedfb37f844d389571d36af681340f41f9477b9ccf34bcc7599bdef222d1d72e79c64373001fae77089fba6d965 + languageName: node + linkType: hard + +"postcss-scss@npm:^4.0.9": + version: 4.0.9 + resolution: "postcss-scss@npm:4.0.9" + peerDependencies: + postcss: ^8.4.29 + checksum: 10c0/f917ecfd4b9113a6648e966a41f027ff7e14238393914978d44596e227a50f084667dc8818742348dc7d8b20130b30d4259aca1d4db86754a9c141202ae03714 + languageName: node + linkType: hard + +"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.16, postcss-selector-parser@npm:^6.1.2": version: 6.1.2 resolution: "postcss-selector-parser@npm:6.1.2" dependencies: @@ -16557,6 +16805,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.47": + version: 8.4.49 + resolution: "postcss@npm:8.4.49" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/f1b3f17aaf36d136f59ec373459f18129908235e65dbdc3aee5eef8eba0756106f52de5ec4682e29a2eab53eb25170e7e871b3e4b52a8f1de3d344a514306be3 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -16564,6 +16823,15 @@ __metadata: languageName: node linkType: hard +"prettier-linter-helpers@npm:^1.0.0": + version: 1.0.0 + resolution: "prettier-linter-helpers@npm:1.0.0" + dependencies: + fast-diff: "npm:^1.1.2" + checksum: 10c0/81e0027d731b7b3697ccd2129470ed9913ecb111e4ec175a12f0fcfab0096516373bf0af2fef132af50cafb0a905b74ff57996d615f59512bb9ac7378fcc64ab + languageName: node + linkType: hard + "prettier@npm:^3.0.1": version: 3.0.1 resolution: "prettier@npm:3.0.1" @@ -17490,6 +17758,13 @@ __metadata: languageName: node linkType: hard +"resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 10c0/b21cb7f1fb746de8107b9febab60095187781137fd803e6a59a76d421444b1531b641bba5857f5dc011974d8a5c635d61cec49e6bd3b7fc20e01f0fafc4efbf2 + languageName: node + linkType: hard + "resolve-pathname@npm:^3.0.0": version: 3.0.0 resolution: "resolve-pathname@npm:3.0.0" @@ -18495,7 +18770,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -18761,6 +19036,149 @@ __metadata: languageName: node linkType: hard +"stylelint-config-prettier-scss@npm:^1.0.0": + version: 1.0.0 + resolution: "stylelint-config-prettier-scss@npm:1.0.0" + peerDependencies: + stylelint: ">=15.0.0" + bin: + stylelint-config-prettier-scss: bin/check.js + stylelint-config-prettier-scss-check: bin/check.js + checksum: 10c0/4d5e1d1c200d4611b5b7bd2d2528cc9e301f26645802a2774aec192c4c2949cbf5a0147eba8b2e6e4ff14a071b03024f3034bb1b4fda37a8ed5a0081a9597d4d + languageName: node + linkType: hard + +"stylelint-config-recommended-scss@npm:^14.0.0": + version: 14.1.0 + resolution: "stylelint-config-recommended-scss@npm:14.1.0" + dependencies: + postcss-scss: "npm:^4.0.9" + stylelint-config-recommended: "npm:^14.0.1" + stylelint-scss: "npm:^6.4.0" + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.6.1 + peerDependenciesMeta: + postcss: + optional: true + checksum: 10c0/0a1c1bb6d9f7a21acea82e12fee1b36a195181ae1dd0d8b59145a56f76232a80d5b706269bc4ca4929680d36f10371bd8a7d0aeeee468fa9119a3b56410b052f + languageName: node + linkType: hard + +"stylelint-config-recommended@npm:^14.0.1": + version: 14.0.1 + resolution: "stylelint-config-recommended@npm:14.0.1" + peerDependencies: + stylelint: ^16.1.0 + checksum: 10c0/a0a0ecd91f4d193bbe2cc3408228f8a2d8fcb2b2578d77233f86780c9247c796a04e16aad7a91d97cb918e2de34b6a8062bab66ee017c3835d855081d94f4828 + languageName: node + linkType: hard + +"stylelint-config-standard-scss@npm:^13.1.0": + version: 13.1.0 + resolution: "stylelint-config-standard-scss@npm:13.1.0" + dependencies: + stylelint-config-recommended-scss: "npm:^14.0.0" + stylelint-config-standard: "npm:^36.0.0" + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.3.1 + peerDependenciesMeta: + postcss: + optional: true + checksum: 10c0/d07cae806ee8b3e77684f019a8b22cc32642373da8053e6ae7ed716f8ddbe6ea1f7323633a6a1bbc9aa08c6a3dceb1dcf053d83fdd10d076b5a01da6e86801ae + languageName: node + linkType: hard + +"stylelint-config-standard@npm:^36.0.0": + version: 36.0.1 + resolution: "stylelint-config-standard@npm:36.0.1" + dependencies: + stylelint-config-recommended: "npm:^14.0.1" + peerDependencies: + stylelint: ^16.1.0 + checksum: 10c0/7f9b954694358e77be5110418f31335be579ce59dd952bc3c6a9449265297db3170ec520e0905769253b48b99c3109a95c71f5b985bf402e48fd6c89b5364cb2 + languageName: node + linkType: hard + +"stylelint-prettier@npm:^5.0.2": + version: 5.0.2 + resolution: "stylelint-prettier@npm:5.0.2" + dependencies: + prettier-linter-helpers: "npm:^1.0.0" + peerDependencies: + prettier: ">=3.0.0" + stylelint: ">=16.0.0" + checksum: 10c0/4755471cd8c8f3d308b25c3ad5db9eb1546dea6f6ba048ca0b83e9ca58169d29456deb50cfa13198a7f7178f9b7e64fddcbb7d0dd0c59669680f5bc6432c438b + languageName: node + linkType: hard + +"stylelint-scss@npm:^6.4.0": + version: 6.9.0 + resolution: "stylelint-scss@npm:6.9.0" + dependencies: + css-tree: "npm:^3.0.1" + is-plain-object: "npm:^5.0.0" + known-css-properties: "npm:^0.35.0" + mdn-data: "npm:^2.12.2" + postcss-media-query-parser: "npm:^0.2.3" + postcss-resolve-nested-selector: "npm:^0.1.6" + postcss-selector-parser: "npm:^6.1.2" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + stylelint: ^16.0.2 + checksum: 10c0/2b0a882589b2be252e730b933e0b9839a7734cd0c5fae8255511835d3d0c5eede171e750dc8ec35229f22a0a3f6e46279adb9dd655544e2fbed1f9fc25bf470a + languageName: node + linkType: hard + +"stylelint@npm:^16.10.0": + version: 16.10.0 + resolution: "stylelint@npm:16.10.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^3.0.1" + "@csstools/css-tokenizer": "npm:^3.0.1" + "@csstools/media-query-list-parser": "npm:^3.0.1" + "@csstools/selector-specificity": "npm:^4.0.0" + "@dual-bundle/import-meta-resolve": "npm:^4.1.0" + balanced-match: "npm:^2.0.0" + colord: "npm:^2.9.3" + cosmiconfig: "npm:^9.0.0" + css-functions-list: "npm:^3.2.3" + css-tree: "npm:^3.0.0" + debug: "npm:^4.3.7" + fast-glob: "npm:^3.3.2" + fastest-levenshtein: "npm:^1.0.16" + file-entry-cache: "npm:^9.1.0" + global-modules: "npm:^2.0.0" + globby: "npm:^11.1.0" + globjoin: "npm:^0.1.4" + html-tags: "npm:^3.3.1" + ignore: "npm:^6.0.2" + imurmurhash: "npm:^0.1.4" + is-plain-object: "npm:^5.0.0" + known-css-properties: "npm:^0.34.0" + mathml-tag-names: "npm:^2.1.3" + meow: "npm:^13.2.0" + micromatch: "npm:^4.0.8" + normalize-path: "npm:^3.0.0" + picocolors: "npm:^1.0.1" + postcss: "npm:^8.4.47" + postcss-resolve-nested-selector: "npm:^0.1.6" + postcss-safe-parser: "npm:^7.0.1" + postcss-selector-parser: "npm:^6.1.2" + postcss-value-parser: "npm:^4.2.0" + resolve-from: "npm:^5.0.0" + string-width: "npm:^4.2.3" + supports-hyperlinks: "npm:^3.1.0" + svg-tags: "npm:^1.0.0" + table: "npm:^6.8.2" + write-file-atomic: "npm:^5.0.1" + bin: + stylelint: bin/stylelint.mjs + checksum: 10c0/d07dd156c225d16c740995daacd78090f7fc317602e87bda2fca323a4ae427a8526d724f3089df3b2185df4520f987547668ceea9b30985988ccbc514034aa21 + languageName: node + linkType: hard + "stylis@npm:^4.1.3": version: 4.3.4 resolution: "stylis@npm:4.3.4" @@ -18784,7 +19202,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.1.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -18802,6 +19220,16 @@ __metadata: languageName: node linkType: hard +"supports-hyperlinks@npm:^3.1.0": + version: 3.1.0 + resolution: "supports-hyperlinks@npm:3.1.0" + dependencies: + has-flag: "npm:^4.0.0" + supports-color: "npm:^7.0.0" + checksum: 10c0/78cc3e17eb27e6846fa355a8ebf343befe36272899cd409e45317a06c1997e95c23ff99d91080a517bd8c96508d4fa456e6ceb338c02ba5d7544277dbec0f10f + languageName: node + linkType: hard + "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -18816,6 +19244,13 @@ __metadata: languageName: node linkType: hard +"svg-tags@npm:^1.0.0": + version: 1.0.0 + resolution: "svg-tags@npm:1.0.0" + checksum: 10c0/5867e29e8f431bf7aecf5a244d1af5725f80a1086187dbc78f26d8433b5e96b8fe9361aeb10d1699ff483b9afec785a10916b9312fe9d734d1a7afd48226c954 + languageName: node + linkType: hard + "svgo@npm:^3.0.2": version: 3.0.2 resolution: "svgo@npm:3.0.2" @@ -18849,6 +19284,19 @@ __metadata: languageName: node linkType: hard +"table@npm:^6.8.2": + version: 6.8.2 + resolution: "table@npm:6.8.2" + dependencies: + ajv: "npm:^8.0.1" + lodash.truncate: "npm:^4.4.2" + slice-ansi: "npm:^4.0.0" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/f8b348af38ee34e419d8ce7306ba00671ce6f20e861ccff22555f491ba264e8416086063ce278a8d81abfa8d23b736ec2cca7ac4029b5472f63daa4b4688b803 + languageName: node + linkType: hard + "tapable@npm:^1.0.0": version: 1.1.3 resolution: "tapable@npm:1.1.3" @@ -20253,6 +20701,16 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^5.0.1": + version: 5.0.1 + resolution: "write-file-atomic@npm:5.0.1" + dependencies: + imurmurhash: "npm:^0.1.4" + signal-exit: "npm:^4.0.1" + checksum: 10c0/e8c850a8e3e74eeadadb8ad23c9d9d63e4e792bd10f4836ed74189ef6e996763959f1249c5650e232f3c77c11169d239cbfc8342fc70f3fe401407d23810505d + languageName: node + linkType: hard + "ws@npm:^7.3.1": version: 7.5.10 resolution: "ws@npm:7.5.10"