@charcoal-ui/react 4.2.0 → 4.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_lib/index.d.ts.map +1 -1
- package/dist/components/Button/index.d.ts +2 -2
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Clickable/index.d.ts +2 -2
- package/dist/components/Clickable/index.d.ts.map +1 -1
- package/dist/components/DropdownSelector/ListItem/index.d.ts +2 -2
- package/dist/components/DropdownSelector/ListItem/index.d.ts.map +1 -1
- package/dist/components/DropdownSelector/MenuList/MenuListContext.d.ts +1 -1
- package/dist/components/DropdownSelector/MenuList/MenuListContext.d.ts.map +1 -1
- package/dist/components/DropdownSelector/Popover/index.d.ts +2 -2
- package/dist/components/DropdownSelector/Popover/index.d.ts.map +1 -1
- package/dist/components/IconButton/index.d.ts +2 -2
- package/dist/components/IconButton/index.d.ts.map +1 -1
- package/dist/components/Modal/useCustomModalOverlay.d.ts +1 -1
- package/dist/components/Modal/useCustomModalOverlay.d.ts.map +1 -1
- package/dist/components/Radio/RadioGroup/index.d.ts +1 -0
- package/dist/components/Radio/RadioGroup/index.d.ts.map +1 -1
- package/dist/components/TagItem/index.d.ts +2 -2
- package/dist/components/TagItem/index.d.ts.map +1 -1
- package/dist/components/TextField/index.d.ts.map +1 -1
- package/dist/components/TextField/useFocusWithClick.d.ts +1 -1
- package/dist/components/TextField/useFocusWithClick.d.ts.map +1 -1
- package/dist/core/SetThemeScript.d.ts +18 -0
- package/dist/core/SetThemeScript.d.ts.map +1 -0
- package/dist/core/themeHelper.d.ts +39 -0
- package/dist/core/themeHelper.d.ts.map +1 -0
- package/dist/index.cjs.js +424 -233
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +315 -137
- package/dist/index.esm.js.map +1 -1
- package/dist/layered.css +1 -1
- package/dist/layered.css.map +1 -1
- package/package.json +11 -9
- package/src/_lib/index.ts +2 -0
- package/src/components/Button/index.tsx +7 -1
- package/src/components/Clickable/index.tsx +1 -1
- package/src/components/DropdownSelector/ListItem/index.tsx +1 -1
- package/src/components/DropdownSelector/MenuList/MenuListContext.ts +1 -1
- package/src/components/DropdownSelector/Popover/index.tsx +3 -2
- package/src/components/IconButton/index.tsx +30 -30
- package/src/components/Modal/__snapshots__/index.story.storyshot +0 -5
- package/src/components/Modal/useCustomModalOverlay.tsx +1 -1
- package/src/components/Radio/RadioGroup/index.tsx +1 -1
- package/src/components/SegmentedControl/__snapshots__/index.story.storyshot +0 -7
- package/src/components/TagItem/index.tsx +55 -55
- package/src/components/TextArea/index.css +1 -1
- package/src/components/TextField/index.tsx +2 -4
- package/src/components/TextField/text-field.test.tsx +72 -0
- package/src/components/TextField/useFocusWithClick.tsx +2 -2
- package/src/core/SetThemeScript.tsx +57 -0
- package/src/core/themeHelper.ts +180 -0
- package/src/index.ts +11 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, memo, useMemo, ForwardedRef } from 'react'
|
|
1
|
+
import React, { forwardRef, memo, useMemo, ForwardedRef, type JSX } from 'react'
|
|
2
2
|
import { useObjectRef } from '@react-aria/utils'
|
|
3
3
|
import Icon from '../Icon'
|
|
4
4
|
import { useClassNames } from '../../_lib/useClassNames'
|
|
@@ -24,66 +24,66 @@ export type TagItemProps<T extends React.ElementType = 'button'> = {
|
|
|
24
24
|
component?: T
|
|
25
25
|
} & Omit<React.ComponentPropsWithRef<T>, 'children'>
|
|
26
26
|
|
|
27
|
-
const TagItem = forwardRef
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
const TagItem = forwardRef<HTMLButtonElement, TagItemProps>(
|
|
28
|
+
function TagItemInner<T extends React.ElementType>(
|
|
29
|
+
{
|
|
30
|
+
component,
|
|
31
|
+
label,
|
|
32
|
+
translatedLabel,
|
|
33
|
+
bgColor = '#7ACCB1',
|
|
34
|
+
bgImage,
|
|
35
|
+
size = 'M',
|
|
36
|
+
status = 'default',
|
|
37
|
+
...props
|
|
38
|
+
}: TagItemProps<T>,
|
|
39
|
+
_ref: ForwardedRef<HTMLButtonElement>
|
|
40
|
+
) {
|
|
41
|
+
const ref = useObjectRef(_ref)
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const hasTranslatedLabel =
|
|
44
|
+
translatedLabel !== undefined && translatedLabel.length > 0
|
|
45
|
+
const className = useClassNames(
|
|
46
|
+
'charcoal-tag-item',
|
|
47
|
+
'charcoal-tag-item__bg',
|
|
48
|
+
props.className
|
|
49
|
+
)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const bgVariant =
|
|
52
|
+
bgImage !== undefined && bgImage.length > 0 ? 'image' : 'color'
|
|
53
|
+
const bg = bgVariant === 'color' ? bgColor : `url(${bgImage ?? ''})`
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
const Component = useMemo(() => component ?? 'button', [component])
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
>
|
|
66
|
-
<div
|
|
67
|
-
className="charcoal-tag-item__label"
|
|
68
|
-
data-has-translate={hasTranslatedLabel}
|
|
57
|
+
return (
|
|
58
|
+
<Component
|
|
59
|
+
{...props}
|
|
60
|
+
ref={ref}
|
|
61
|
+
className={className}
|
|
62
|
+
data-state={status}
|
|
63
|
+
data-bg-variant={bgVariant}
|
|
64
|
+
data-size={hasTranslatedLabel ? 'M' : size}
|
|
65
|
+
style={{ '--charcoal-tag-item-bg': bg }}
|
|
69
66
|
>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{translatedLabel}
|
|
73
|
-
</span>
|
|
74
|
-
)}
|
|
75
|
-
<span
|
|
76
|
-
className="charcoal-tag-item__label__text"
|
|
67
|
+
<div
|
|
68
|
+
className="charcoal-tag-item__label"
|
|
77
69
|
data-has-translate={hasTranslatedLabel}
|
|
78
70
|
>
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
{hasTranslatedLabel && (
|
|
72
|
+
<span className="charcoal-tag-item__label__translated">
|
|
73
|
+
{translatedLabel}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
<span
|
|
77
|
+
className="charcoal-tag-item__label__text"
|
|
78
|
+
data-has-translate={hasTranslatedLabel}
|
|
79
|
+
>
|
|
80
|
+
{label}
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
{status === 'active' && <Icon name="16/Remove" />}
|
|
84
|
+
</Component>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
) as <T extends React.ElementType = 'button'>(p: TagItemProps<T>) => JSX.Element
|
|
88
88
|
|
|
89
89
|
export default memo(TagItem)
|
|
@@ -108,9 +108,7 @@ const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
108
108
|
data-invalid={invalid === true}
|
|
109
109
|
ref={containerRef}
|
|
110
110
|
>
|
|
111
|
-
{prefix
|
|
112
|
-
<div className="charcoal-text-field-prefix">{prefix}</div>
|
|
113
|
-
)}
|
|
111
|
+
{prefix && <div className="charcoal-text-field-prefix">{prefix}</div>}
|
|
114
112
|
<input
|
|
115
113
|
className="charcoal-text-field-input"
|
|
116
114
|
aria-describedby={showAssistiveText ? describedbyId : undefined}
|
|
@@ -126,7 +124,7 @@ const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
126
124
|
value={value}
|
|
127
125
|
{...props}
|
|
128
126
|
/>
|
|
129
|
-
{(suffix
|
|
127
|
+
{(suffix || showCount) && (
|
|
130
128
|
<div className="charcoal-text-field-suffix">
|
|
131
129
|
{suffix}
|
|
132
130
|
{showCount && (
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import TextField from '.'
|
|
3
|
+
|
|
4
|
+
import '@testing-library/jest-dom'
|
|
5
|
+
|
|
6
|
+
describe('TextField component', () => {
|
|
7
|
+
it('should not render prefix and suffix when not provided', () => {
|
|
8
|
+
const { container } = render(<TextField />)
|
|
9
|
+
|
|
10
|
+
// prefix and suffix elements should not be rendered
|
|
11
|
+
const prefixElement = container.querySelector('.charcoal-text-field-prefix')
|
|
12
|
+
const suffixElement = container.querySelector('.charcoal-text-field-suffix')
|
|
13
|
+
|
|
14
|
+
expect(prefixElement).toBeNull()
|
|
15
|
+
expect(suffixElement).toBeNull()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test.each([
|
|
19
|
+
[null, 'null'],
|
|
20
|
+
[undefined, 'undefined'],
|
|
21
|
+
['', 'empty string'],
|
|
22
|
+
[false, 'boolean false'],
|
|
23
|
+
[0, 'zero'],
|
|
24
|
+
])(
|
|
25
|
+
'should not render prefix when value is falsy (%s: %s)',
|
|
26
|
+
(prefixValue, _desc) => {
|
|
27
|
+
const { container } = render(<TextField prefix={prefixValue} />)
|
|
28
|
+
const prefixElement = container.querySelector('.charcoal-text-prefix')
|
|
29
|
+
expect(prefixElement).toBeNull()
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
test.each([
|
|
34
|
+
[null, 'null'],
|
|
35
|
+
[undefined, 'undefined'],
|
|
36
|
+
['', 'empty string'],
|
|
37
|
+
[false, 'boolean false'],
|
|
38
|
+
[0, 'zero'],
|
|
39
|
+
])(
|
|
40
|
+
'should not render suffix when value is falsy (%s: %s) and showCount is false',
|
|
41
|
+
(suffixValue, _desc) => {
|
|
42
|
+
const { container } = render(
|
|
43
|
+
<TextField suffix={suffixValue} showCount={false} />
|
|
44
|
+
)
|
|
45
|
+
const suffixElement = container.querySelector(
|
|
46
|
+
'.charcoal-text-field-suffix'
|
|
47
|
+
)
|
|
48
|
+
expect(suffixElement).toBeNull()
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
it('should render prefix and suffix when provided as truthy values', () => {
|
|
53
|
+
const prefixContent = 'Test Prefix'
|
|
54
|
+
const suffixContent = 'Test Suffix'
|
|
55
|
+
const { container, getByText } = render(
|
|
56
|
+
<TextField
|
|
57
|
+
prefix={<span>{prefixContent}</span>}
|
|
58
|
+
suffix={<span>{suffixContent}</span>}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const prefixElement = container.querySelector('.charcoal-text-field-prefix')
|
|
63
|
+
const suffixElement = container.querySelector('.charcoal-text-field-suffix')
|
|
64
|
+
|
|
65
|
+
expect(prefixElement).not.toBeNull()
|
|
66
|
+
expect(suffixElement).not.toBeNull()
|
|
67
|
+
|
|
68
|
+
// Verify text content
|
|
69
|
+
expect(getByText(prefixContent)).toBeInTheDocument()
|
|
70
|
+
expect(getByText(suffixContent)).toBeInTheDocument()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -2,8 +2,8 @@ import { useEffect } from 'react'
|
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
|
|
4
4
|
export function useFocusWithClick(
|
|
5
|
-
containerRef: React.RefObject<HTMLDivElement>,
|
|
6
|
-
inputRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement>
|
|
5
|
+
containerRef: React.RefObject<HTMLDivElement | null>,
|
|
6
|
+
inputRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>
|
|
7
7
|
) {
|
|
8
8
|
useEffect(() => {
|
|
9
9
|
const el = containerRef.current
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertKeyString,
|
|
3
|
+
DEFAULT_ROOT_ATTRIBUTE,
|
|
4
|
+
LOCAL_STORAGE_KEY,
|
|
5
|
+
} from './themeHelper'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
localStorageKey: string
|
|
9
|
+
rootAttribute: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 同期的にテーマをローカルストレージから取得してhtmlの属性に設定するコードを取得する
|
|
14
|
+
* @param props localStorageのキー、htmlのdataになる属性のキーを含むオブジェクト
|
|
15
|
+
* @returns ソースコードの文字列
|
|
16
|
+
*/
|
|
17
|
+
export function makeSetThemeScriptCode({
|
|
18
|
+
localStorageKey = defaultProps.localStorageKey,
|
|
19
|
+
rootAttribute = defaultProps.rootAttribute,
|
|
20
|
+
}: Partial<Props> = defaultProps) {
|
|
21
|
+
assertKeyString(localStorageKey)
|
|
22
|
+
assertKeyString(rootAttribute)
|
|
23
|
+
return `'use strict';
|
|
24
|
+
(function () {
|
|
25
|
+
var localStorageKey = '${localStorageKey}'
|
|
26
|
+
var rootAttribute = '${rootAttribute}'
|
|
27
|
+
var currentTheme = localStorage.getItem(localStorageKey);
|
|
28
|
+
if (currentTheme) {
|
|
29
|
+
document.documentElement.dataset[rootAttribute] = currentTheme;
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 同期的にテーマをローカルストレージから取得してhtmlの属性に設定するスクリプトタグ
|
|
37
|
+
* @param props localStorageのキー、htmlのdataになる属性のキーを含むオブジェクト
|
|
38
|
+
* @returns
|
|
39
|
+
*/
|
|
40
|
+
export function SetThemeScript({
|
|
41
|
+
localStorageKey = defaultProps.localStorageKey,
|
|
42
|
+
rootAttribute = defaultProps.rootAttribute,
|
|
43
|
+
}: Props) {
|
|
44
|
+
const src = makeSetThemeScriptCode({ localStorageKey, rootAttribute })
|
|
45
|
+
return (
|
|
46
|
+
<script
|
|
47
|
+
dangerouslySetInnerHTML={{
|
|
48
|
+
__html: src,
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const defaultProps: Props = {
|
|
55
|
+
localStorageKey: LOCAL_STORAGE_KEY,
|
|
56
|
+
rootAttribute: DEFAULT_ROOT_ATTRIBUTE,
|
|
57
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export const LOCAL_STORAGE_KEY = 'charcoal-theme'
|
|
4
|
+
export const DEFAULT_ROOT_ATTRIBUTE = 'theme'
|
|
5
|
+
|
|
6
|
+
const keyStringRegExp = new RegExp(/^(\w|-)+$/)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 文字列が英数字_-のみで構成されているか検証する。不正な文字列ならエラーを投げる
|
|
10
|
+
* @param key 検証するキー
|
|
11
|
+
*/
|
|
12
|
+
export function assertKeyString(key: string) {
|
|
13
|
+
if (!keyStringRegExp.test(key)) {
|
|
14
|
+
throw new Error(`Unexpected key :${key}, expect: /^(\\w|-)+$/`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `<html data-theme="dark">` のような設定を行うデフォルトのセッター
|
|
20
|
+
*/
|
|
21
|
+
export const themeSetter =
|
|
22
|
+
(attr: string = DEFAULT_ROOT_ATTRIBUTE) =>
|
|
23
|
+
(theme: string | undefined) => {
|
|
24
|
+
assertKeyString(attr)
|
|
25
|
+
if (theme !== undefined) {
|
|
26
|
+
document.documentElement.dataset[attr] = theme
|
|
27
|
+
} else {
|
|
28
|
+
delete document.documentElement.dataset[attr]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `<html data-theme="dark">` にマッチするセレクタを生成する
|
|
34
|
+
*/
|
|
35
|
+
export function themeSelector<
|
|
36
|
+
T extends string,
|
|
37
|
+
S extends string = typeof DEFAULT_ROOT_ATTRIBUTE
|
|
38
|
+
>(theme: T, attr?: S) {
|
|
39
|
+
return `:root[data-${attr ?? DEFAULT_ROOT_ATTRIBUTE}='${theme}']` as const
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* prefers-color-scheme を利用する media クエリを生成する
|
|
44
|
+
*/
|
|
45
|
+
export function prefersColorScheme<T extends 'light' | 'dark'>(theme: T) {
|
|
46
|
+
return `@media (prefers-color-scheme: ${theme})` as const
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* LocalStorageからテーマの情報を取得して、変化時にテーマをセットするhooks
|
|
51
|
+
*/
|
|
52
|
+
export function useThemeSetter({
|
|
53
|
+
key = LOCAL_STORAGE_KEY,
|
|
54
|
+
setter = themeSetter(),
|
|
55
|
+
}: { key?: string; setter?: (theme: string | undefined) => void } = {}) {
|
|
56
|
+
const [theme, , system] = useTheme(key)
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (theme === undefined) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
// prefers-color-scheme から値を取っている場合にはcssのみで処理したいのでアンセットする
|
|
63
|
+
setter(system ? undefined : theme)
|
|
64
|
+
}, [setter, system, theme])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 同期的にLocalStorageからテーマを取得するヘルパ
|
|
69
|
+
*/
|
|
70
|
+
export function getThemeSync(key: string = LOCAL_STORAGE_KEY) {
|
|
71
|
+
const theme = localStorage.getItem(key)
|
|
72
|
+
return theme
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* LocalStorage, prefers-color-scheme からテーマの情報を取得して、現在のテーマを返すhooks
|
|
77
|
+
*
|
|
78
|
+
* `dark` `light` という名前だけは特別扱いされていて、prefers-color-schemeにマッチした場合に返ります
|
|
79
|
+
*/
|
|
80
|
+
export const useTheme = (localStorageKey: string = LOCAL_STORAGE_KEY) => {
|
|
81
|
+
assertKeyString(localStorageKey)
|
|
82
|
+
const isDark = useMedia('(prefers-color-scheme: dark)')
|
|
83
|
+
const media = isDark !== undefined ? (isDark ? 'dark' : 'light') : undefined
|
|
84
|
+
const [local, setTheme, ready] =
|
|
85
|
+
useLocalStorage<typeof media>(localStorageKey)
|
|
86
|
+
const theme = !ready || media === undefined ? undefined : local ?? media
|
|
87
|
+
const system = local === undefined
|
|
88
|
+
return [theme, setTheme, system] as const
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function useLocalStorage<T>(key: string, defaultValue?: () => T) {
|
|
92
|
+
const [ready, setReady] = useState(false)
|
|
93
|
+
const [state, setState] = useState<T>()
|
|
94
|
+
const defaultValueMemo = useMemo(() => defaultValue?.(), [defaultValue])
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
fetch()
|
|
98
|
+
window.addEventListener('storage', handleStorage)
|
|
99
|
+
return () => {
|
|
100
|
+
window.removeEventListener('storage', handleStorage)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const handleStorage = (e: StorageEvent) => {
|
|
105
|
+
if (e.storageArea !== localStorage) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (e.key !== key) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
fetch()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fetch = () => {
|
|
115
|
+
const raw = localStorage.getItem(key)
|
|
116
|
+
setState((raw !== null ? deserialize(raw) : null) ?? defaultValueMemo)
|
|
117
|
+
setReady(true)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const set = (value: T | undefined) => {
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
// undefinedがセットされる場合にはkeyごと削除
|
|
123
|
+
localStorage.removeItem(key)
|
|
124
|
+
} else {
|
|
125
|
+
const raw = serialize(value)
|
|
126
|
+
localStorage.setItem(key, raw)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 同一ウィンドウではstorageイベントが発火しないので、手動で発火させる
|
|
130
|
+
const event = new StorageEvent('storage', {
|
|
131
|
+
bubbles: true,
|
|
132
|
+
cancelable: false,
|
|
133
|
+
key,
|
|
134
|
+
url: location.href,
|
|
135
|
+
storageArea: localStorage,
|
|
136
|
+
})
|
|
137
|
+
dispatchEvent(event)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return [state ?? defaultValueMemo, set, ready] as const
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function deserialize<T>(raw: string): T {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(raw) as T
|
|
146
|
+
} catch {
|
|
147
|
+
// syntax error はすべて文字列として扱う
|
|
148
|
+
return raw as unknown as T
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function serialize(value: unknown): string {
|
|
153
|
+
if (typeof value === 'string') {
|
|
154
|
+
return value
|
|
155
|
+
} else {
|
|
156
|
+
return JSON.stringify(value)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function useMedia(query: string) {
|
|
161
|
+
const [match, setState] = useState<boolean>()
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const matcher = window.matchMedia(query)
|
|
165
|
+
|
|
166
|
+
const onChange = () => {
|
|
167
|
+
setState(matcher.matches)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
matcher.addEventListener('change', onChange)
|
|
171
|
+
|
|
172
|
+
setState(matcher.matches)
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
matcher.removeEventListener('change', onChange)
|
|
176
|
+
}
|
|
177
|
+
}, [query])
|
|
178
|
+
|
|
179
|
+
return match
|
|
180
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,17 @@ export {
|
|
|
4
4
|
CharcoalProvider,
|
|
5
5
|
type CharcoalProviderProps,
|
|
6
6
|
} from './core/CharcoalProvider'
|
|
7
|
+
export { makeSetThemeScriptCode, SetThemeScript } from './core/SetThemeScript'
|
|
8
|
+
export {
|
|
9
|
+
getThemeSync,
|
|
10
|
+
themeSetter,
|
|
11
|
+
themeSelector,
|
|
12
|
+
prefersColorScheme,
|
|
13
|
+
useTheme,
|
|
14
|
+
useThemeSetter,
|
|
15
|
+
useLocalStorage,
|
|
16
|
+
useMedia,
|
|
17
|
+
} from './core/themeHelper'
|
|
7
18
|
export { default as Button, type ButtonProps } from './components/Button'
|
|
8
19
|
export {
|
|
9
20
|
default as Clickable,
|