Skip to content

Commit 798ec9d

Browse files
committed
Fix select options if disabled
1 parent 7e5862e commit 798ec9d

File tree

7 files changed

+139
-18
lines changed

7 files changed

+139
-18
lines changed

preview/src/components/select/style.css

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
.select-option[data-disabled="true"] {
134134
color: var(--secondary-color-5);
135135
cursor: not-allowed;
136+
opacity: 0.5;
136137
}
137138

138139
.select-option:hover:not([data-disabled="true"]),
@@ -148,8 +149,3 @@
148149
color: var(--secondary-color-5);
149150
font-size: 0.75rem;
150151
}
151-
152-
[data-disabled="true"] {
153-
cursor: not-allowed;
154-
opacity: 0.5;
155-
}

primitives/src/select/components/list.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,19 +125,19 @@ pub fn SelectList(props: SelectListProps) -> Element {
125125
}
126126
Key::ArrowUp => {
127127
arrow_key_navigation(event);
128-
ctx.focus_state.focus_prev();
128+
ctx.focus_prev();
129129
}
130130
Key::End => {
131131
arrow_key_navigation(event);
132-
ctx.focus_state.focus_last();
132+
ctx.focus_last();
133133
}
134134
Key::ArrowDown => {
135135
arrow_key_navigation(event);
136-
ctx.focus_state.focus_next();
136+
ctx.focus_next();
137137
}
138138
Key::Home => {
139139
arrow_key_navigation(event);
140-
ctx.focus_state.focus_first();
140+
ctx.focus_first();
141141
}
142142
Key::Enter => {
143143
ctx.select_current_item();
@@ -163,9 +163,13 @@ pub fn SelectList(props: SelectListProps) -> Element {
163163

164164
use_effect(move || {
165165
if render() {
166-
ctx.focus_state.set_focus(ctx.initial_focus.cloned());
166+
if (ctx.initial_focus_last)().unwrap_or_default() {
167+
ctx.focus_last();
168+
} else {
169+
ctx.focus_first();
170+
}
167171
} else {
168-
ctx.initial_focus.set(None);
172+
ctx.initial_focus_last.set(None);
169173
}
170174
});
171175

primitives/src/select/components/option.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,14 @@ pub fn SelectOption<T: PartialEq + Clone + 'static>(props: SelectOptionProps<T>)
129129

130130
// Push this option to the context
131131
let mut ctx: SelectContext = use_context();
132+
let disabled = ctx.disabled.cloned() || props.disabled.cloned();
132133
use_effect(move || {
133134
let option_state = OptionState {
134135
tab_index: index(),
135136
value: RcPartialEqValue::new(value.cloned()),
136137
text_value: text_value.cloned(),
137138
id: id(),
139+
disabled
138140
};
139141

140142
// Add the option to the context's options
@@ -147,7 +149,6 @@ pub fn SelectOption<T: PartialEq + Clone + 'static>(props: SelectOptionProps<T>)
147149

148150
let onmounted = use_focus_controlled_item(props.index);
149151
let focused = move || ctx.focus_state.is_focused(index());
150-
let disabled = ctx.disabled.cloned() || props.disabled.cloned();
151152
let selected = use_memo(move || {
152153
ctx.value.read().as_ref().and_then(|v| v.as_ref::<T>()) == Some(&props.value.read())
153154
});
@@ -172,6 +173,9 @@ pub fn SelectOption<T: PartialEq + Clone + 'static>(props: SelectOptionProps<T>)
172173
aria_label: props.aria_label.clone(),
173174
aria_roledescription: props.aria_roledescription.clone(),
174175

176+
// data attributes
177+
"data-disabled": disabled,
178+
175179
onpointerdown: move |event| {
176180
if !disabled && event.trigger_button() == Some(MouseButton::Primary) {
177181
ctx.set_value.call(Some(RcPartialEqValue::new(props.value.cloned())));

primitives/src/select/components/select.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ pub fn Select<T: Clone + PartialEq + 'static>(props: SelectProps<T>) -> Element
143143
typeahead_buffer.take();
144144
}
145145
});
146-
let initial_focus = use_signal(|| None);
146+
let initial_focus_last = use_signal(|| None);
147147

148148
use_context_provider(|| SelectContext {
149149
typeahead_buffer,
@@ -158,7 +158,7 @@ pub fn Select<T: Clone + PartialEq + 'static>(props: SelectProps<T>) -> Element
158158
placeholder: props.placeholder,
159159
typeahead_clear_task,
160160
typeahead_timeout: props.typeahead_timeout,
161-
initial_focus,
161+
initial_focus_last,
162162
});
163163

164164
rsx! {

primitives/src/select/components/trigger.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ pub fn SelectTrigger(props: SelectTriggerProps) -> Element {
8282
match event.key() {
8383
Key::ArrowUp => {
8484
open.set(true);
85-
ctx.initial_focus.set(ctx.focus_state.item_count().checked_sub(1));
85+
ctx.initial_focus_last.set(Some(true));
8686
event.prevent_default();
8787
event.stop_propagation();
8888
}
8989
Key::ArrowDown => {
9090
open.set(true);
91-
ctx.initial_focus.set((ctx.focus_state.item_count() > 0).then_some(0));
91+
ctx.initial_focus_last.set(Some(false));
9292
event.prevent_default();
9393
event.stop_propagation();
9494
}

primitives/src/select/context.rs

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,119 @@ pub(super) struct SelectContext {
7373
pub typeahead_clear_task: Signal<Option<Task>>,
7474
/// Timeout before clearing typeahead buffer
7575
pub typeahead_timeout: ReadSignal<Duration>,
76-
/// The initial element to focus once the list is rendered
77-
pub initial_focus: Signal<Option<usize>>,
76+
77+
/// The initial element to focus once the list is rendered<br>
78+
/// true: last element<br>
79+
/// false: first element
80+
pub initial_focus_last: Signal<Option<bool>>,
7881
}
7982

8083
impl SelectContext {
84+
/// custom implementation for `FocusState::focus_next`
85+
pub(crate) fn focus_next(&mut self) {
86+
let current_focus = self.focus_state.recent_focus();
87+
let mut new_focus = current_focus.unwrap_or_default();
88+
let start_focus = current_focus.unwrap_or_default();
89+
let item_count = (self.focus_state.item_count)();
90+
let roving_loop = (self.focus_state.roving_loop)();
91+
let options = self.options.read();
92+
93+
loop {
94+
new_focus = new_focus.saturating_add(1);
95+
if new_focus >= item_count {
96+
new_focus = match roving_loop {
97+
true => 0,
98+
false => item_count.saturating_sub(1),
99+
}
100+
}
101+
102+
// get value of the option
103+
let disabled = options.iter().find(|opt| opt.tab_index == new_focus).map(|e| e.disabled).unwrap_or(false);
104+
105+
// this fails if the current_focus at the start is None
106+
if !disabled || new_focus == start_focus {
107+
break;
108+
}
109+
}
110+
111+
self.focus_state.set_focus(Some(new_focus));
112+
}
113+
114+
/// custom implementation for `FocusState::focus_prev`
115+
pub(crate) fn focus_prev(&mut self) {
116+
let current_focus = self.focus_state.recent_focus();
117+
let mut new_focus = current_focus.unwrap_or_default();
118+
let start_focus = current_focus.unwrap_or_default();
119+
let item_count = (self.focus_state.item_count)();
120+
let roving_loop = (self.focus_state.roving_loop)();
121+
let options = self.options.read();
122+
123+
loop {
124+
let old_focus = new_focus;
125+
new_focus = new_focus.saturating_sub(1);
126+
127+
if old_focus == 0 && roving_loop {
128+
new_focus = item_count.saturating_sub(1);
129+
}
130+
131+
// get value of the option
132+
let disabled = options.iter().find(|opt| opt.tab_index == new_focus).map(|e| e.disabled).unwrap_or(false);
133+
134+
if !disabled || new_focus == start_focus {
135+
break;
136+
}
137+
}
138+
139+
self.focus_state.set_focus(Some(new_focus));
140+
}
141+
142+
/// custom implementation for `FocusState::focus_last`
143+
pub(crate) fn focus_last(&mut self) {
144+
let item_count = (self.focus_state.item_count)();
145+
let options = self.options.read();
146+
let mut new_focus = item_count;
147+
148+
loop {
149+
// If at the start, don't focus anything
150+
if new_focus == 0 {
151+
return;
152+
}
153+
new_focus = new_focus.saturating_sub(1);
154+
155+
// get value of the option
156+
let disabled = options.iter().find(|opt| opt.tab_index == new_focus).map(|e| e.disabled).unwrap_or(false);
157+
158+
if !disabled {
159+
break;
160+
}
161+
}
162+
self.focus_state.set_focus(Some(new_focus));
163+
}
164+
165+
/// custom implementation for `FocusState::focus_first`
166+
pub(crate) fn focus_first(&mut self) {
167+
let item_count = (self.focus_state.item_count)();
168+
let options = self.options.read();
169+
let mut new_focus = 0;
170+
171+
loop {
172+
// get value of the option
173+
let disabled = options.iter().find(|opt| opt.tab_index == new_focus).map(|e| e.disabled).unwrap_or(false);
174+
175+
if !disabled {
176+
break;
177+
}
178+
179+
// If at the end, don't focus anything
180+
if new_focus >= item_count {
181+
return;
182+
}
183+
184+
new_focus = new_focus.saturating_add(1);
185+
}
186+
self.focus_state.set_focus(Some(new_focus));
187+
}
188+
81189
/// Select the currently focused item
82190
pub fn select_current_item(&mut self) {
83191
// If the select is open, select the focused item
@@ -154,6 +262,9 @@ pub(super) struct OptionState {
154262
pub text_value: String,
155263
/// Unique ID for the option
156264
pub id: String,
265+
266+
/// Whether the option is disabled
267+
pub disabled: bool,
157268
}
158269

159270
/// Context for select option components to know if they're selected

primitives/src/select/text_search.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub(super) fn best_match(
1818

1919
options
2020
.iter()
21+
.filter(|o| !o.disabled)
2122
.map(|opt| {
2223
let value = &opt.text_value;
2324
let value_characters: Box<[_]> = value.chars().collect();
@@ -539,18 +540,21 @@ mod tests {
539540
value: RcPartialEqValue::new("apple"),
540541
text_value: "Apple".to_string(),
541542
id: "apple".to_string(),
543+
disabled: false,
542544
},
543545
OptionState {
544546
tab_index: 1,
545547
value: RcPartialEqValue::new("banana"),
546548
text_value: "Banana".to_string(),
547549
id: "banana".to_string(),
550+
disabled: false,
548551
},
549552
OptionState {
550553
tab_index: 2,
551554
value: RcPartialEqValue::new("cherry"),
552555
text_value: "Cherry".to_string(),
553556
id: "cherry".to_string(),
557+
disabled: false,
554558
},
555559
];
556560

@@ -605,12 +609,14 @@ mod tests {
605609
value: RcPartialEqValue::new("ф"),
606610
text_value: "ф".to_string(),
607611
id: "ф".to_string(),
612+
disabled: false,
608613
},
609614
OptionState {
610615
tab_index: 1,
611616
value: RcPartialEqValue::new("banana"),
612617
text_value: "Banana".to_string(),
613618
id: "banana".to_string(),
619+
disabled: false,
614620
},
615621
];
616622

0 commit comments

Comments
 (0)