melta-app 0.4.3 → 0.5.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/lib/module/components/Card.js +32 -18
- package/lib/module/components/Card.js.map +1 -1
- package/lib/module/contracts/contract-types.js +2 -2
- package/lib/typescript/src/components/Card.d.ts +27 -4
- package/lib/typescript/src/components/Card.d.ts.map +1 -1
- package/lib/typescript/src/contracts/contract-types.d.ts +2 -2
- package/lib/typescript/src/primitives/Button.d.ts +1 -1
- package/package.json +2 -2
- package/src/components/Card.tsx +40 -14
- package/src/contracts/contract-types.ts +2 -2
|
@@ -11,6 +11,23 @@
|
|
|
11
11
|
* action/link は pressed 時 elevation.sm→md(§2 hover→pressed mapping)。
|
|
12
12
|
* focus-within は RN 非サポートとして Phase1 明示 drop(§2)。
|
|
13
13
|
* - 型で action/link は onPress 必須を強制(§4 の DU 精神。無言で非対話に落ちる事故を防ぐ)。
|
|
14
|
+
* - **カード面自体は操作要素にしない**(contract 2.1.0 の a11y.required)。面の Pressable は
|
|
15
|
+
* `accessibilityRole` を名乗らず `accessible={false}`。理由は2つ:
|
|
16
|
+
* 1. 面に button role を付けると、中に Button を置いたとき web で `<button>` の入れ子になり
|
|
17
|
+
* hydration error になる(production では minified React error #418 に潰れて原因が読めない)
|
|
18
|
+
* 2. 面はキーボードでもスクリーンリーダーでも押せない。面のタップは**ポインタ向けの近道**で、
|
|
19
|
+
* 他の入力手段からの到達は内包する `primaryAction` が担う
|
|
20
|
+
* そのため action/link は `primaryAction` を必須にする。書き忘れると「ポインタでしか押せない
|
|
21
|
+
* カード」が静かに出来上がるので、型で止める。
|
|
22
|
+
* - `role="article"` は contract の a11y.role をそのまま表現したもの(RN の W3C 準拠 prop。
|
|
23
|
+
* react-native-web では実際に `<article>` 要素になる)。accessibilityRole 系とは別系統。
|
|
24
|
+
* - **focus ring は Card では描かない**。フォーカスを受けるのは `primaryAction` 側で、
|
|
25
|
+
* melta の Button は自前の ring を持つ。カードにも出すと1回のフォーカスで ring が二重に描かれる。
|
|
26
|
+
* contract の focus-within は RN 非サポートとして Phase1 で明示 drop 済み(app recipe に記録あり)で、
|
|
27
|
+
* web だけ動く実装を残すとプラットフォーム非対称になるため、実装ごと持たない。
|
|
28
|
+
* - `accessibilityLabel` は 0.5.0 で削除した。面が `accessible={false}` なので native では
|
|
29
|
+
* 原理的に読まれず、web だけ `aria-label` として効く非対称な prop だった。
|
|
30
|
+
* 名前は操作要素が持つべきなので `primaryAction` 側に付ける。
|
|
14
31
|
* - Card は Image/Metric に依存しない(§1)。media/header/footer/children に呼び出し側が差し込む。
|
|
15
32
|
* 呼び出し側(例: D2I)は用途に応じて slot に Image+Metric を compose する。
|
|
16
33
|
* - media variant は overflow:hidden で角丸に画像をクリップし、body だけにパディング(contract と整合)。
|
|
@@ -18,11 +35,11 @@
|
|
|
18
35
|
|
|
19
36
|
import { Pressable, View } from "react-native";
|
|
20
37
|
import { useTheme } from "../theme/index.js";
|
|
21
|
-
import { useFocusRing, FocusRing } from "../primitives/_internal/focus-ring.js";
|
|
22
38
|
import { CONTRACTS } from "../contracts/contract-types.js";
|
|
23
39
|
import { CARD_INTERACTIVE, resolveCardShape, resolveCardBodyStyle } from "./card.styles.js";
|
|
24
40
|
|
|
25
|
-
// action/link(インタラクティブ)は onPress
|
|
41
|
+
// action/link(インタラクティブ)は onPress と primaryAction が必須、
|
|
42
|
+
// basic/media はどちらも取らない(§4 DU 精神)。
|
|
26
43
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
27
44
|
export function Card({
|
|
28
45
|
variant = "basic",
|
|
@@ -30,7 +47,7 @@ export function Card({
|
|
|
30
47
|
footer,
|
|
31
48
|
media,
|
|
32
49
|
onPress,
|
|
33
|
-
|
|
50
|
+
primaryAction,
|
|
34
51
|
style,
|
|
35
52
|
testID,
|
|
36
53
|
children
|
|
@@ -39,10 +56,6 @@ export function Card({
|
|
|
39
56
|
theme,
|
|
40
57
|
mode
|
|
41
58
|
} = useTheme();
|
|
42
|
-
const {
|
|
43
|
-
focused,
|
|
44
|
-
focusHandlers
|
|
45
|
-
} = useFocusRing();
|
|
46
59
|
const interactive = CARD_INTERACTIVE[variant] && onPress != null;
|
|
47
60
|
const isMedia = variant === "media";
|
|
48
61
|
const inner = /*#__PURE__*/_jsxs(_Fragment, {
|
|
@@ -50,7 +63,7 @@ export function Card({
|
|
|
50
63
|
children: media
|
|
51
64
|
}), /*#__PURE__*/_jsxs(View, {
|
|
52
65
|
style: resolveCardBodyStyle(theme, variant),
|
|
53
|
-
children: [header, children, footer]
|
|
66
|
+
children: [header, children, footer, primaryAction]
|
|
54
67
|
})]
|
|
55
68
|
});
|
|
56
69
|
|
|
@@ -65,26 +78,27 @@ export function Card({
|
|
|
65
78
|
}) : inner;
|
|
66
79
|
if (!interactive) {
|
|
67
80
|
return /*#__PURE__*/_jsx(View, {
|
|
81
|
+
role: "article",
|
|
68
82
|
testID: testID,
|
|
69
83
|
style: [resolveCardShape(theme, mode, variant), style],
|
|
70
84
|
children: content
|
|
71
85
|
});
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
// action/link: pressed 時 elevation.sm→md
|
|
75
|
-
return /*#__PURE__*/
|
|
76
|
-
|
|
77
|
-
onPress: onPress
|
|
78
|
-
accessibilityRole
|
|
79
|
-
|
|
88
|
+
// action/link: pressed 時 elevation.sm→md(focus 表示は primaryAction 側が持つ)。
|
|
89
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
90
|
+
role: "article",
|
|
91
|
+
onPress: onPress
|
|
92
|
+
// 面は「操作要素」ではなく article(contract 2.1.0)。accessibilityRole は名乗らない。
|
|
93
|
+
// accessible={false} で a11y ツリーから子を畳まないことで、内包する primaryAction が
|
|
94
|
+
// 独立した操作要素として読み上げられる(既定の Pressable は子を畳んでしまう)。
|
|
95
|
+
,
|
|
96
|
+
accessible: false,
|
|
80
97
|
testID: testID,
|
|
81
98
|
style: ({
|
|
82
99
|
pressed
|
|
83
100
|
}) => [resolveCardShape(theme, mode, variant, pressed), style],
|
|
84
|
-
children:
|
|
85
|
-
visible: focused,
|
|
86
|
-
radius: theme.radius.lg
|
|
87
|
-
})]
|
|
101
|
+
children: content
|
|
88
102
|
});
|
|
89
103
|
}
|
|
90
104
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Pressable","View","useTheme","
|
|
1
|
+
{"version":3,"names":["Pressable","View","useTheme","CONTRACTS","CARD_INTERACTIVE","resolveCardShape","resolveCardBodyStyle","jsx","_jsx","jsxs","_jsxs","Fragment","_Fragment","Card","variant","header","footer","media","onPress","primaryAction","style","testID","children","theme","mode","interactive","isMedia","inner","content","borderRadius","radius","lg","overflow","role","accessible","pressed","__contract","card"],"sourceRoot":"../../../src","sources":["components/Card.tsx"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA,SAASA,SAAS,EAAEC,IAAI,QAAwC,cAAc;AAC9E,SAASC,QAAQ,QAAQ,mBAAU;AACnC,SAASC,SAAS,QAAQ,gCAA6B;AACvD,SAASC,gBAAgB,EAAEC,gBAAgB,EAAEC,oBAAoB,QAAQ,kBAAe;;AAYxF;AACA;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA,EAAAC,QAAA,IAAAC,SAAA;AAgBA,OAAO,SAASC,IAAIA,CAAC;EACnBC,OAAO,GAAG,OAAO;EACjBC,MAAM;EACNC,MAAM;EACNC,KAAK;EACLC,OAAO;EACPC,aAAa;EACbC,KAAK;EACLC,MAAM;EACNC;AACS,CAAC,EAAE;EACZ,MAAM;IAAEC,KAAK;IAAEC;EAAK,CAAC,GAAGtB,QAAQ,CAAC,CAAC;EAClC,MAAMuB,WAAW,GAAGrB,gBAAgB,CAACU,OAAO,CAAC,IAAII,OAAO,IAAI,IAAI;EAChE,MAAMQ,OAAO,GAAGZ,OAAO,KAAK,OAAO;EAEnC,MAAMa,KAAK,gBACTjB,KAAA,CAAAE,SAAA;IAAAU,QAAA,GACGL,KAAK,IAAI,IAAI,iBAAIT,IAAA,CAACP,IAAI;MAAAqB,QAAA,EAAEL;IAAK,CAAO,CAAC,eACtCP,KAAA,CAACT,IAAI;MAACmB,KAAK,EAAEd,oBAAoB,CAACiB,KAAK,EAAET,OAAO,CAAE;MAAAQ,QAAA,GAC/CP,MAAM,EACNO,QAAQ,EACRN,MAAM,EACNG,aAAa;IAAA,CACV,CAAC;EAAA,CACP,CACH;;EAED;EACA;EACA,MAAMS,OAAO,GAAGF,OAAO,gBACrBlB,IAAA,CAACP,IAAI;IAACmB,KAAK,EAAE;MAAES,YAAY,EAAEN,KAAK,CAACO,MAAM,CAACC,EAAE;MAAEC,QAAQ,EAAE;IAAS,CAAE;IAAAV,QAAA,EAAEK;EAAK,CAAO,CAAC,GAElFA,KACD;EAED,IAAI,CAACF,WAAW,EAAE;IAChB,oBACEjB,IAAA,CAACP,IAAI;MAACgC,IAAI,EAAC,SAAS;MAACZ,MAAM,EAAEA,MAAO;MAACD,KAAK,EAAE,CAACf,gBAAgB,CAACkB,KAAK,EAAEC,IAAI,EAAEV,OAAO,CAAC,EAAEM,KAAK,CAAE;MAAAE,QAAA,EACzFM;IAAO,CACJ,CAAC;EAEX;;EAEA;EACA,oBACEpB,IAAA,CAACR,SAAS;IACRiC,IAAI,EAAC,SAAS;IACdf,OAAO,EAAEA;IACT;IACA;IACA;IAAA;IACAgB,UAAU,EAAE,KAAM;IAClBb,MAAM,EAAEA,MAAO;IACfD,KAAK,EAAEA,CAAC;MAAEe;IAAQ,CAAC,KAAK,CAAC9B,gBAAgB,CAACkB,KAAK,EAAEC,IAAI,EAAEV,OAAO,EAAEqB,OAAO,CAAC,EAAEf,KAAK,CAAE;IAAAE,QAAA,EAEhFM;EAAO,CACC,CAAC;AAEhB;;AAEA;AACAf,IAAI,CAACuB,UAAU,GAAGjC,SAAS,CAACkC,IAAI","ignoreList":[]}
|
|
@@ -16,7 +16,7 @@ export const CONTRACTS = {
|
|
|
16
16
|
},
|
|
17
17
|
button: {
|
|
18
18
|
id: "button",
|
|
19
|
-
version: "2.
|
|
19
|
+
version: "2.1.0",
|
|
20
20
|
variants: ["contained", "outlined", "brand-outline", "neutral", "lighted", "danger", "subtle"],
|
|
21
21
|
sizes: ["small", "medium", "large"],
|
|
22
22
|
states: ["default", "hover", "focus", "disabled", "loading"]
|
|
@@ -30,7 +30,7 @@ export const CONTRACTS = {
|
|
|
30
30
|
},
|
|
31
31
|
card: {
|
|
32
32
|
id: "card",
|
|
33
|
-
version: "2.
|
|
33
|
+
version: "2.1.0",
|
|
34
34
|
variants: ["basic", "media", "action", "link"],
|
|
35
35
|
sizes: [],
|
|
36
36
|
states: ["default", "hover", "focus-within"]
|
|
@@ -9,17 +9,33 @@
|
|
|
9
9
|
* action/link は pressed 時 elevation.sm→md(§2 hover→pressed mapping)。
|
|
10
10
|
* focus-within は RN 非サポートとして Phase1 明示 drop(§2)。
|
|
11
11
|
* - 型で action/link は onPress 必須を強制(§4 の DU 精神。無言で非対話に落ちる事故を防ぐ)。
|
|
12
|
+
* - **カード面自体は操作要素にしない**(contract 2.1.0 の a11y.required)。面の Pressable は
|
|
13
|
+
* `accessibilityRole` を名乗らず `accessible={false}`。理由は2つ:
|
|
14
|
+
* 1. 面に button role を付けると、中に Button を置いたとき web で `<button>` の入れ子になり
|
|
15
|
+
* hydration error になる(production では minified React error #418 に潰れて原因が読めない)
|
|
16
|
+
* 2. 面はキーボードでもスクリーンリーダーでも押せない。面のタップは**ポインタ向けの近道**で、
|
|
17
|
+
* 他の入力手段からの到達は内包する `primaryAction` が担う
|
|
18
|
+
* そのため action/link は `primaryAction` を必須にする。書き忘れると「ポインタでしか押せない
|
|
19
|
+
* カード」が静かに出来上がるので、型で止める。
|
|
20
|
+
* - `role="article"` は contract の a11y.role をそのまま表現したもの(RN の W3C 準拠 prop。
|
|
21
|
+
* react-native-web では実際に `<article>` 要素になる)。accessibilityRole 系とは別系統。
|
|
22
|
+
* - **focus ring は Card では描かない**。フォーカスを受けるのは `primaryAction` 側で、
|
|
23
|
+
* melta の Button は自前の ring を持つ。カードにも出すと1回のフォーカスで ring が二重に描かれる。
|
|
24
|
+
* contract の focus-within は RN 非サポートとして Phase1 で明示 drop 済み(app recipe に記録あり)で、
|
|
25
|
+
* web だけ動く実装を残すとプラットフォーム非対称になるため、実装ごと持たない。
|
|
26
|
+
* - `accessibilityLabel` は 0.5.0 で削除した。面が `accessible={false}` なので native では
|
|
27
|
+
* 原理的に読まれず、web だけ `aria-label` として効く非対称な prop だった。
|
|
28
|
+
* 名前は操作要素が持つべきなので `primaryAction` 側に付ける。
|
|
12
29
|
* - Card は Image/Metric に依存しない(§1)。media/header/footer/children に呼び出し側が差し込む。
|
|
13
30
|
* 呼び出し側(例: D2I)は用途に応じて slot に Image+Metric を compose する。
|
|
14
31
|
* - media variant は overflow:hidden で角丸に画像をクリップし、body だけにパディング(contract と整合)。
|
|
15
32
|
*/
|
|
16
|
-
import type { ReactNode } from "react";
|
|
33
|
+
import type { ReactElement, ReactNode } from "react";
|
|
17
34
|
import { type StyleProp, type ViewStyle } from "react-native";
|
|
18
35
|
interface CardBase {
|
|
19
36
|
header?: ReactNode;
|
|
20
37
|
footer?: ReactNode;
|
|
21
38
|
media?: ReactNode;
|
|
22
|
-
accessibilityLabel?: string;
|
|
23
39
|
style?: StyleProp<ViewStyle>;
|
|
24
40
|
testID?: string;
|
|
25
41
|
/** body。 */
|
|
@@ -28,15 +44,22 @@ interface CardBase {
|
|
|
28
44
|
type CardProps = CardBase & ({
|
|
29
45
|
variant?: "basic" | "media";
|
|
30
46
|
onPress?: never;
|
|
47
|
+
primaryAction?: never;
|
|
31
48
|
} | {
|
|
32
49
|
variant: "action" | "link";
|
|
33
50
|
onPress: () => void;
|
|
51
|
+
/**
|
|
52
|
+
* 面のタップと同じ遷移先を持つ操作要素(Button 等)。footer 位置に描画される。
|
|
53
|
+
* 面は role を持たないので、キーボード / スクリーンリーダーからの唯一の到達手段になる。
|
|
54
|
+
* 内包する操作要素が他にもある場合、ここに置くのは**主アクション**(contract 2.1.0)。
|
|
55
|
+
*/
|
|
56
|
+
primaryAction: ReactElement;
|
|
34
57
|
});
|
|
35
|
-
export declare function Card({ variant, header, footer, media, onPress,
|
|
58
|
+
export declare function Card({ variant, header, footer, media, onPress, primaryAction, style, testID, children, }: CardProps): import("react/jsx-runtime").JSX.Element;
|
|
36
59
|
export declare namespace Card {
|
|
37
60
|
var __contract: {
|
|
38
61
|
readonly id: "card";
|
|
39
|
-
readonly version: "2.
|
|
62
|
+
readonly version: "2.1.0";
|
|
40
63
|
readonly variants: readonly ["basic", "media", "action", "link"];
|
|
41
64
|
readonly sizes: readonly [];
|
|
42
65
|
readonly states: readonly ["default", "hover", "focus-within"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Card.d.ts","sourceRoot":"","sources":["../../../../src/components/Card.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"Card.d.ts","sourceRoot":"","sources":["../../../../src/components/Card.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAmB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAK/E,UAAU,QAAQ;IAChB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY;IACZ,QAAQ,EAAE,SAAS,CAAC;CACrB;AAID,KAAK,SAAS,GAAG,QAAQ,GACvB,CACI;IAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,KAAK,CAAC;IAAC,aAAa,CAAC,EAAE,KAAK,CAAA;CAAE,GACvE;IACE,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB;;;;OAIG;IACH,aAAa,EAAE,YAAY,CAAC;CAC7B,CACJ,CAAC;AAEJ,wBAAgB,IAAI,CAAC,EACnB,OAAiB,EACjB,MAAM,EACN,MAAM,EACN,KAAK,EACL,OAAO,EACP,aAAa,EACb,KAAK,EACL,MAAM,EACN,QAAQ,GACT,EAAE,SAAS,2CAgDX;yBA1De,IAAI"}
|
|
@@ -16,7 +16,7 @@ export declare const CONTRACTS: {
|
|
|
16
16
|
};
|
|
17
17
|
readonly button: {
|
|
18
18
|
readonly id: "button";
|
|
19
|
-
readonly version: "2.
|
|
19
|
+
readonly version: "2.1.0";
|
|
20
20
|
readonly variants: readonly ["contained", "outlined", "brand-outline", "neutral", "lighted", "danger", "subtle"];
|
|
21
21
|
readonly sizes: readonly ["small", "medium", "large"];
|
|
22
22
|
readonly states: readonly ["default", "hover", "focus", "disabled", "loading"];
|
|
@@ -30,7 +30,7 @@ export declare const CONTRACTS: {
|
|
|
30
30
|
};
|
|
31
31
|
readonly card: {
|
|
32
32
|
readonly id: "card";
|
|
33
|
-
readonly version: "2.
|
|
33
|
+
readonly version: "2.1.0";
|
|
34
34
|
readonly variants: readonly ["basic", "media", "action", "link"];
|
|
35
35
|
readonly sizes: readonly [];
|
|
36
36
|
readonly states: readonly ["default", "hover", "focus-within"];
|
|
@@ -38,7 +38,7 @@ export declare function Button(props: ButtonProps): import("react/jsx-runtime").
|
|
|
38
38
|
export declare namespace Button {
|
|
39
39
|
var __contract: {
|
|
40
40
|
readonly id: "button";
|
|
41
|
-
readonly version: "2.
|
|
41
|
+
readonly version: "2.1.0";
|
|
42
42
|
readonly variants: readonly ["contained", "outlined", "brand-outline", "neutral", "lighted", "danger", "subtle"];
|
|
43
43
|
readonly sizes: readonly ["small", "medium", "large"];
|
|
44
44
|
readonly states: readonly ["default", "hover", "focus", "disabled", "loading"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "melta-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "melta for APP — melta デザインシステムの React Native UI kit。melta-contracts(デザイン契約)を single source of truth に RN 実装でその契約を満たす。アプリ本体ではなくライブラリ。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://app.melta.tsubotax.com",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@types/react": "~19.2.2",
|
|
75
75
|
"eslint": "^10.4.1",
|
|
76
76
|
"jest": "^29.7.0",
|
|
77
|
-
"melta-contracts": "^0.
|
|
77
|
+
"melta-contracts": "^0.6.0",
|
|
78
78
|
"react": "19.2.3",
|
|
79
79
|
"react-native": "0.85.3",
|
|
80
80
|
"react-native-builder-bob": "^0.40.13",
|
package/src/components/Card.tsx
CHANGED
|
@@ -9,15 +9,31 @@
|
|
|
9
9
|
* action/link は pressed 時 elevation.sm→md(§2 hover→pressed mapping)。
|
|
10
10
|
* focus-within は RN 非サポートとして Phase1 明示 drop(§2)。
|
|
11
11
|
* - 型で action/link は onPress 必須を強制(§4 の DU 精神。無言で非対話に落ちる事故を防ぐ)。
|
|
12
|
+
* - **カード面自体は操作要素にしない**(contract 2.1.0 の a11y.required)。面の Pressable は
|
|
13
|
+
* `accessibilityRole` を名乗らず `accessible={false}`。理由は2つ:
|
|
14
|
+
* 1. 面に button role を付けると、中に Button を置いたとき web で `<button>` の入れ子になり
|
|
15
|
+
* hydration error になる(production では minified React error #418 に潰れて原因が読めない)
|
|
16
|
+
* 2. 面はキーボードでもスクリーンリーダーでも押せない。面のタップは**ポインタ向けの近道**で、
|
|
17
|
+
* 他の入力手段からの到達は内包する `primaryAction` が担う
|
|
18
|
+
* そのため action/link は `primaryAction` を必須にする。書き忘れると「ポインタでしか押せない
|
|
19
|
+
* カード」が静かに出来上がるので、型で止める。
|
|
20
|
+
* - `role="article"` は contract の a11y.role をそのまま表現したもの(RN の W3C 準拠 prop。
|
|
21
|
+
* react-native-web では実際に `<article>` 要素になる)。accessibilityRole 系とは別系統。
|
|
22
|
+
* - **focus ring は Card では描かない**。フォーカスを受けるのは `primaryAction` 側で、
|
|
23
|
+
* melta の Button は自前の ring を持つ。カードにも出すと1回のフォーカスで ring が二重に描かれる。
|
|
24
|
+
* contract の focus-within は RN 非サポートとして Phase1 で明示 drop 済み(app recipe に記録あり)で、
|
|
25
|
+
* web だけ動く実装を残すとプラットフォーム非対称になるため、実装ごと持たない。
|
|
26
|
+
* - `accessibilityLabel` は 0.5.0 で削除した。面が `accessible={false}` なので native では
|
|
27
|
+
* 原理的に読まれず、web だけ `aria-label` として効く非対称な prop だった。
|
|
28
|
+
* 名前は操作要素が持つべきなので `primaryAction` 側に付ける。
|
|
12
29
|
* - Card は Image/Metric に依存しない(§1)。media/header/footer/children に呼び出し側が差し込む。
|
|
13
30
|
* 呼び出し側(例: D2I)は用途に応じて slot に Image+Metric を compose する。
|
|
14
31
|
* - media variant は overflow:hidden で角丸に画像をクリップし、body だけにパディング(contract と整合)。
|
|
15
32
|
*/
|
|
16
33
|
|
|
17
|
-
import type { ReactNode } from "react";
|
|
34
|
+
import type { ReactElement, ReactNode } from "react";
|
|
18
35
|
import { Pressable, View, type StyleProp, type ViewStyle } from "react-native";
|
|
19
36
|
import { useTheme } from "../theme";
|
|
20
|
-
import { useFocusRing, FocusRing } from "../primitives/_internal/focus-ring";
|
|
21
37
|
import { CONTRACTS } from "../contracts/contract-types";
|
|
22
38
|
import { CARD_INTERACTIVE, resolveCardShape, resolveCardBodyStyle } from "./card.styles";
|
|
23
39
|
|
|
@@ -25,18 +41,27 @@ interface CardBase {
|
|
|
25
41
|
header?: ReactNode;
|
|
26
42
|
footer?: ReactNode;
|
|
27
43
|
media?: ReactNode;
|
|
28
|
-
accessibilityLabel?: string;
|
|
29
44
|
style?: StyleProp<ViewStyle>;
|
|
30
45
|
testID?: string;
|
|
31
46
|
/** body。 */
|
|
32
47
|
children: ReactNode;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
|
-
// action/link(インタラクティブ)は onPress
|
|
50
|
+
// action/link(インタラクティブ)は onPress と primaryAction が必須、
|
|
51
|
+
// basic/media はどちらも取らない(§4 DU 精神)。
|
|
36
52
|
type CardProps = CardBase &
|
|
37
53
|
(
|
|
38
|
-
| { variant?: "basic" | "media"; onPress?: never }
|
|
39
|
-
| {
|
|
54
|
+
| { variant?: "basic" | "media"; onPress?: never; primaryAction?: never }
|
|
55
|
+
| {
|
|
56
|
+
variant: "action" | "link";
|
|
57
|
+
onPress: () => void;
|
|
58
|
+
/**
|
|
59
|
+
* 面のタップと同じ遷移先を持つ操作要素(Button 等)。footer 位置に描画される。
|
|
60
|
+
* 面は role を持たないので、キーボード / スクリーンリーダーからの唯一の到達手段になる。
|
|
61
|
+
* 内包する操作要素が他にもある場合、ここに置くのは**主アクション**(contract 2.1.0)。
|
|
62
|
+
*/
|
|
63
|
+
primaryAction: ReactElement;
|
|
64
|
+
}
|
|
40
65
|
);
|
|
41
66
|
|
|
42
67
|
export function Card({
|
|
@@ -45,13 +70,12 @@ export function Card({
|
|
|
45
70
|
footer,
|
|
46
71
|
media,
|
|
47
72
|
onPress,
|
|
48
|
-
|
|
73
|
+
primaryAction,
|
|
49
74
|
style,
|
|
50
75
|
testID,
|
|
51
76
|
children,
|
|
52
77
|
}: CardProps) {
|
|
53
78
|
const { theme, mode } = useTheme();
|
|
54
|
-
const { focused, focusHandlers } = useFocusRing();
|
|
55
79
|
const interactive = CARD_INTERACTIVE[variant] && onPress != null;
|
|
56
80
|
const isMedia = variant === "media";
|
|
57
81
|
|
|
@@ -62,6 +86,7 @@ export function Card({
|
|
|
62
86
|
{header}
|
|
63
87
|
{children}
|
|
64
88
|
{footer}
|
|
89
|
+
{primaryAction}
|
|
65
90
|
</View>
|
|
66
91
|
</>
|
|
67
92
|
);
|
|
@@ -76,24 +101,25 @@ export function Card({
|
|
|
76
101
|
|
|
77
102
|
if (!interactive) {
|
|
78
103
|
return (
|
|
79
|
-
<View testID={testID} style={[resolveCardShape(theme, mode, variant), style]}>
|
|
104
|
+
<View role="article" testID={testID} style={[resolveCardShape(theme, mode, variant), style]}>
|
|
80
105
|
{content}
|
|
81
106
|
</View>
|
|
82
107
|
);
|
|
83
108
|
}
|
|
84
109
|
|
|
85
|
-
// action/link: pressed 時 elevation.sm→md
|
|
110
|
+
// action/link: pressed 時 elevation.sm→md(focus 表示は primaryAction 側が持つ)。
|
|
86
111
|
return (
|
|
87
112
|
<Pressable
|
|
88
|
-
|
|
113
|
+
role="article"
|
|
89
114
|
onPress={onPress}
|
|
90
|
-
accessibilityRole
|
|
91
|
-
|
|
115
|
+
// 面は「操作要素」ではなく article(contract 2.1.0)。accessibilityRole は名乗らない。
|
|
116
|
+
// accessible={false} で a11y ツリーから子を畳まないことで、内包する primaryAction が
|
|
117
|
+
// 独立した操作要素として読み上げられる(既定の Pressable は子を畳んでしまう)。
|
|
118
|
+
accessible={false}
|
|
92
119
|
testID={testID}
|
|
93
120
|
style={({ pressed }) => [resolveCardShape(theme, mode, variant, pressed), style]}
|
|
94
121
|
>
|
|
95
122
|
{content}
|
|
96
|
-
<FocusRing visible={focused} radius={theme.radius.lg} />
|
|
97
123
|
</Pressable>
|
|
98
124
|
);
|
|
99
125
|
}
|
|
@@ -21,7 +21,7 @@ export const CONTRACTS = {
|
|
|
21
21
|
},
|
|
22
22
|
button: {
|
|
23
23
|
id: "button",
|
|
24
|
-
version: "2.
|
|
24
|
+
version: "2.1.0",
|
|
25
25
|
variants: ["contained", "outlined", "brand-outline", "neutral", "lighted", "danger", "subtle"],
|
|
26
26
|
sizes: ["small", "medium", "large"],
|
|
27
27
|
states: ["default", "hover", "focus", "disabled", "loading"],
|
|
@@ -35,7 +35,7 @@ export const CONTRACTS = {
|
|
|
35
35
|
},
|
|
36
36
|
card: {
|
|
37
37
|
id: "card",
|
|
38
|
-
version: "2.
|
|
38
|
+
version: "2.1.0",
|
|
39
39
|
variants: ["basic", "media", "action", "link"],
|
|
40
40
|
sizes: [],
|
|
41
41
|
states: ["default", "hover", "focus-within"],
|